Python高级系列教程:Python闭包和装饰器

今天我们将继续讲解 Python 中的闭包和装饰器。虽然我们还没有详细学习这两个概念,但在面向对象编程中,我们已经经常使用装饰器了。装饰器可以给函数增加额外的功能,就像语法糖一样甜。在 Python 中,装饰器的格式通常是在函数上方加上 @ 符号。

下面看一下本章的学习目标:

  • 能够知道闭包的构成条件

  • 能够知道定义闭包的基本语法

  • 能够知道修改闭包内使用的外部变量所需要的关键字

  • 能够知道定义装饰器的基本语法及其作用

  • 能够写出通用的装饰器

  • 能够使用多个装饰器装饰一个函数

  • 能够写出带有参数的装饰器

  • 能够知道类装饰器的使用

闭包

下面我们将向大家介绍闭包的概念。在此之前,我们需要先解释一下变量在内存中的存储方式。如我们之前所述,Python代码中的变量分为两种类型:全局变量和局部变量。闭包与局部变量有关。接下来,我们将详细讲解闭包。

变量在内存中的存储形式

前言:全局变量与局部变量的访问

全局变量的访问范围:

①在全局作用域中可以正常访问全局变量

②在局部作用域中也可以正常访问全局变量

局部变量的访问范围:

①在局部作用域中可以正常访问局部变量

②在全局作用域中无法正常访问局部变量,因为函数执行完毕后,其内部的变量会随之消失。 在代码中展示如下:

 
 
 
 

'''
作用域就是变量的作用范围,在哪里可以使用,在哪里不可以使用。
Python中变量随着函数的出现,作用域可以划分两种情况:① 全局作用域 ② 局部作用域
随着作用域的出现,变量也被强制划分两种类型:① 全局变量 ② 局部变量
① 全局变量:在全局作用域中定义的变量就是全局变量
② 局部变量:在局部作用域中定义的变量就是局部变量
'''
# 全局作用域
num1 = 10 # 全局变量
def func():
# 局部作用域
num2 = 100 # 局部变量

扩展:为什么函数执行完毕,其内部的变量会随之消失呢?答:内存的垃圾回收机制

1、闭包的作用

我们前面已经学过了函数,我们知道当函数调用完,函数内定义的变量都销毁了,但是我们有时候需要保存函数内的这个变量,每次在这个变量的基础上完成一些列的操作,比如: 每次在这个变量的基础上和其它数字进行求和计算,那怎么办呢? 

我们就可以通过咱们今天学习的闭包来解决这个需求。 

闭包的定义: 

在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数的地址,我们把这个使用外部函数变量的内部函数称为闭包。

2、闭包的构成条件

通过闭包的定义,我们可以得知闭包的形成条件: 

①在函数嵌套(函数里面再定义函数)的前提下 

②内部函数使用了外部函数的变量(还包括外部函数的参数) 

③外部函数返回了内部函数

3、闭包的示例代码

 
 
 
 

# 定义一个外部函数
def func_out(num1):
# 定义一个内部函数
def func_inner(num2):
# 内部函数使用了外部函数的变量(num1)
result = num1 + num2
print("结果是:", result)
# 外部函数返回了内部函数,这里返回的内部函数就是闭包
return func_inner
# 创建闭包实例
f = func_out(1)
# 执行闭包
f(2)
f(3)

4、闭包的作用

闭包可以保存外部函数内的变量,不会随着外部函数调用完而销毁。 注意点: 由于闭包引用了外部函数的变量,则外部函数的变量没有及时释放,消耗内存。

5、小结

①当返回的内部函数使用了外部函数的变量就形成了闭包 

②闭包可以对外部函数的变量进行保存 

③实现闭包的标准格式:

 
 
 
 

# 外部函数
def test1(a):
b = 10
# 内部函数
def test2():
# 内部函数使用了外部函数的变量或者参数
print(a, b)
# 返回内部函数, 这里返回的内部函数就是闭包实例
return test2

修改闭包内使用的外部变量

1、修改闭包内使用的外部变量

错误版本演示:

 
 
 
 

def outer():
# outer函数中的局部变量
num = 10
def inner():
num = 20
print(f'原始变量:{num}') # 10
inner()
print(f'修改后的变量:{num}') # 10
return inner

f = outer()

这段代码定义了一个函数outer,它包含了另一个函数inner。在outer函数中,定义了一个局部变量num并赋值为10。在inner函数中,又定义了一个同名的局部变量num并赋值为20。但是由于Python的作用域规则,inner函数中的num只是outer函数中的num的一个新的局部变量,两者并不相互影响。因此,调用inner函数后,outer函数中的num仍然是10。最后,outer函数返回inner函数的引用,并将其赋值给变量f。这样,变量f就可以调用inner函数了。

2、修改闭包内使用的外部变量

正确版本演示:

 
 
 
 

def outer():
# outer函数中的局部变量
num = 10 def inner():
nonlocal num
# inner函数中的局部变量
num = 20
print(f'原始变量:{num}') # 10 inner()
print(f'修改后的变量:{num}') # 20
return inner
f = outer()

这段代码定义了一个函数outer,其中包含一个局部变量num,初始值为10。另外,函数outer还定义了一个内部函数inner。在inner函数中,使用nonlocal关键字声明了num变量是outer函数中的局部变量,而不是inner函数的局部变量。然后,将num的值修改为20,并输出修改前后的值。最后,outer函数返回inner函数的引用,并将其赋值给变量f。这样,变量f就成为了一个指向inner函数的函数对象。

3、闭包综合案例

 
 
 
 

def outer():
result = 0
def inner(num):
nonlocal result
result += num
print(result)
return inner
f = outer()
f(1)
f(2)
f(3) # 问这次的执行结果是多少?

这段代码定义了一个函数outer(),它返回了内部函数inner(),并且inner()可以访问outer()函数内的变量result。 

在函数outer()中,我们定义了一个变量result并将其初始化为0。接着,我们定义了内部函数inner(),它带有一个参数num。在inner()函数内部,我们使用nonlocal关键字来声明result变量是来自outer()函数的变量,并将其与num相加,并在每次加完之后打印result的值。最后, 我们返回内部函数inner()。 

接下来,我们在函数调用时使用f = outer()将inner()函数赋值给了变量f。这意味着我们可以在后续代码中使用f()函数,来调用我们定义的内部函数。 

我们接着多次调用了f()函数,每一次调用都会调用内部函数并将不同的参数传递给它。由于在内部函数中,我们使用了非局部变量result来记录之前的调用结果,因此在第三次调用时,函数会打印出结果为6的结果。所以,这段代码的输出为: 

6

装饰器

1、装饰器的定义

就是给已有函数增加额外功能的函数,它本质上就是一个闭包函数。 装饰器的功能特点: 

①不修改已有函数的源代码 

②不修改已有函数的调用方式 

③给已有函数增加额外的功能

2、装饰器代码

示例代码:

 
 
 
 

# 添加一个登录验证的功能
def check(fn):
def inner():
print("请先登录....")
fn()
return inner


def comment():
print("发表评论")

# 使用装饰器来装饰函数
comment = check(comment)
comment()

这段代码定义了一个装饰器函数check,它接受一个函数作为参数,并返回一个新的函数inner。新的函数inner在执行原函数fn之前,会先打印一条“请先登录”的提示信息。 

接下来,定义了一个函数comment,它用于发表评论。最后,使用装饰器来装饰函数comment,即将函数comment传入check函数中,将返回的新函数inner赋值给comment。 

最后一行代码调用了被装饰后的函数comment,会先执行inner函数中的提示信息,再执行原函数comment中的发表评论的功能。这样就实现了一个简单的登录验证功能。

 
 
 
 

# 装饰器的基本雏形
# def decorator(fn): # fn:目标函数.
# def inner():
# '''执行函数之前'''
# fn() # 执行被装饰的函数
# '''执行函数之后'''
# return inner

这段代码是一个装饰器的基本雏形,它定义了一个名为 decorator 的函数,该函数接受一个参数 fn,即目标函数。decorator 函数内部定义了一个名为 inner 的函数,该函数在执行目标函数之前和之后分别执行一些操作。最后,decorator 函数返回 inner 函数,从而实现对目标函数的装饰。具体来说,当我们在定义一个函数时,可以使用 @decorator 的语法来将 decorator 函数应用到目标函数上,从而实现对目标函数的装饰。

3、装饰器的语法糖写法

  • 如果有多个函数都需要添加登录验证的功能,每次都需要编写func = check(func)这样代码对已有函数进行装饰,这种做法还是比较麻烦。

  • Python给提供了一个装饰函数更加简单的写法,那就是语法糖,语法糖的书写格式是: @装饰器名字,通过语法糖的方式也可以完成对已有函数的装饰

 
 
 
 

# 添加一个登录验证的功能
def check(fn):
print("装饰器函数执行了")
def inner():
print("请先登录....")
fn()
return inner

转换

 
 
 
 

# 使用语法糖方式来装饰函数
@check
def comment():
print("发表评论")

comment()

装饰器的作用

1、装饰器的使用场景

  1. 统计程序的执行时间:可以通过编写一个装饰器来记录函数的开始时间和结束时间,然后计算函数执行的时间。这个装饰器可以被用于优化程序性能或者进行调试。

  2. 辅助系统功能输出日志信息:可以编写一个装饰器来记录函数的执行过程以及输出日志信息,这个装饰器可以被用于调试、监控和错误处理等方面。

2、装饰器实现已有函数执行时间的统计

 
 
 
 

import time
def get_time(func):
def inner():
# 开始计时
begin = time.time()
func()
# 结束计时
end = time.time()
print("函数执行花费%f" % (end - begin))

return inner

这段代码定义了一个装饰器函数get_time,接受一个函数作为参数。装饰器函数内部定义了一个嵌套函数inner,它用于包装传入的函数func。inner函数在调用func之前和之后记录了时间,并计算出函数执行的时间,最后输出函数执行花费的时间。

装饰器函数get_time返回了inner函数,即返回了一个新的函数,这个新函数的功能是在原函数执行前后记录时间并输出执行时间。使用装饰器@get_time可以将这个装饰器应用于需要计时的函数上,从而方便地获得函数执行的时间。 转换为

 
 
 
 

@get_time
def demo():
for i in range(100000):
print(i)

demo()

这段代码定义了一个装饰器函数 @get_time,并将其应用于函数 demo()  上。装饰器函数 @get_time 的作用是在函数 demo() 执行前和执行后分别记录当前时间,并计算两次时间差,最后将时间差输出。 

函数 demo() 中包含一个循环,循环次数为 100000,每次循环输出一个数值。当调用 demo() 函数时,它会执行循环并输出数值,同时也会触发装饰器函数 @get_time 的执行,从而输出函数执行的时间。

通用装饰器的使用

1、装饰器的使用场景

  1. 统计程序的执行时间:可以通过编写一个装饰器来记录函数的开始时间和结束时间,然后计算函数执行的时间。这个装饰器可以被用于优化程序性能或者进行调试。

  2. 辅助系统功能输出日志信息:可以编写一个装饰器来记录函数的执行过程以及输出日志信息,这个装饰器可以被用于调试、监控和错误处理等方面。

2、装饰器实现已有函数执行时间的统计

 
 
 
 

import time
def get_time(func):
def inner():
# 开始计时
begin = time.time()
func()
# 结束计时
end = time.time()
print("函数执行花费%f" % (end - begin))

return inner

这段代码定义了一个装饰器函数get_time,接受一个函数作为参数。装饰器函数内部定义了一个嵌套函数inner,它用于包装传入的函数func。inner函数在调用func之前和之后记录了时间,并计算出函数执行的时间,最后输出函数执行花费的时间。

装饰器函数get_time返回了inner函数,即返回了一个新的函数,这个新函数的功能是在原函数执行前后记录时间并输出执行时间。使用装饰器@get_time可以将这个装饰器应用于需要计时的函数上,从而方便地获得函数执行的时间。 转换为

 
 
 
 

@get_time
def demo():
for i in range(100000):
print(i)

demo()

这段代码定义了一个装饰器函数 @get_time,并将其应用于函数 demo()  上。装饰器函数 @get_time 的作用是在函数 demo() 执行前和执行后分别记录当前时间,并计算两次时间差,最后将时间差输出。 

函数 demo() 中包含一个循环,循环次数为 100000,每次循环输出一个数值。当调用 demo() 函数时,它会执行循环并输出数值,同时也会触发装饰器函数 @get_time 的执行,从而输出函数执行的时间。

通用装饰器的使用

1、装饰带有参数的函数

 
 
 
 

def logging(fn):
def inner(num1, num2):
print('--正在努力计算--')
fn(num1, num2)
return inner

@logging
def sum_num(num1, num2):
result = num1 + num2
print(result)

sum_num(10, 20)

这段代码定义了一个装饰器函数logging,它接受一个函数fn作为参数。logging返回一个内部函数inner,inner接受两个参数num1和num2,并在调用fn之前打印一条信息。最后,logging将inner函数返回。

@logging是一个装饰器语法,它可以将装饰器函数logging应用到下面的函数sum_num上。这意味着sum_num将被重新定义为inner函数,即调用sum_num实际上是调用inner函数。 

在sum_num函数内部,它计算num1和num2的和,并将结果打印出来。由于sum_num被logging装饰器修饰,所以在调用sum_num之前会先打印一条信息。最后,调用sum_num(10, 20)将会输出以下内容: 

--正在努力计算-- 

30

2、装饰带有返回值的函数

 
 
 
 

def logging(fn):
def inner(num1, num2):
print('--正在努力计算--')
result = fn(num1, num2)
return result
return inner

@logging
def sum_num(num1, num2):
result = num1 + num2
return result

print(sum_num(10, 20))

这段代码定义了一个装饰器函数logging,它接受一个函数作为参数,并返回一个新的函数inner。inner函数在执行原函数之前会打印一条信息“--正在努力计算--”,然后执行原函数,最后返回原函数的返回值。 

接下来使用@logging装饰器来修饰函数sum_num,相当于将sum_num函数传递给logging函数,并将返回的新函数重新赋值给sum_num。这样,当调用sum_num函数时,实际上执行的是inner函数。 

最后,调用sum_num函数并传入参数10和20,输出结果为30。同时,也会打印出"--正在努力计算--"这条信息。

3、装饰带有不定长参数的函数

 
 
 
 

def logging(fn):
def inner(*args, **kwargs):
print('--正在努力计算--')
fn(*args, **kwargs)
return inner

这段代码定义了一个装饰器函数 logging,它接受一个函数 fn 作为参数,并返回一个函数 inner。 

函数 inner 接受任意数量的位置参数和关键字参数,并在执行 fn 函数之前打印一条信息 "--正在努力计算--"。然后,函数 inner 调用 fn 函数并传入相同的参数。 

这个装饰器函数可以用来给其他函数添加日志信息,以便在函数执行时打印一些有用的信息,比如函数开始执行、执行结束等。

 
 
 
 

@logging
def sum_num(*args, **kwargs):
result = 0
for i in args:
result += i

for i in kwargs.values():
result += i
print(result)

sum_num(10, 20, a=30)

这段代码定义了一个函数sum_num(),它将任意数量的位置参数和关键字参数作为输入,并将它们加起来。函数内部首先将所有位置参数相加,接着将所有关键字参数的值相加,最后将两者之和打印出来。 

代码中的装饰器@logging表示在函数执行时会调用一个名为logging的函数(这个函数并没有给出),它可能会记录函数的执行情况(比如开始时间、结束时间、参数值等等)。这样,我们就可以在调用函数时进行记录和日志的输出,从而方便我们进行错误调试和问题排查。 

最后,我们对函数进行了一次调用sum_num(10, 20, a=30),输出结果为60。这是由于10与20是位置参数,相加为30,而a=30是一个关键字参数,值为30,所以总和为60。 

运行结果:

4、通用装饰器

 
 
 
 

# 添加输出日志的功能
def logging(fn):
def inner(*args, **kwargs):
print("--正在努力计算--")
result = fn(*args, **kwargs)
return result

return inner

这段代码定义了一个装饰器函数 logging,它接受一个函数 fn 作为参数,并返回一个新函数 inner。 

新函数 inner 接受任意数量的位置参数和关键字参数,并在函数执行前输出一条日志,表示正在计算中。然后调用原始函数 fn 并返回其结果。 

这个装饰器函数可以应用于其他函数,以添加输出日志的功能,使每次函数执行时都会输出一条日志。例如:

 
 
 
 

@logging
def add(a, b):
return a + b

当调用 add(2, 3) 时,会输出 "--正在努力计算--",然后返回 5。

 
 
 
 

# 使用语法糖装饰函数
@logging
def sum_num(*args, **kwargs):
result = 0
for value in args:
result += value

for value in kwargs.values():
result += value

return result


@logging
def subtraction(a, b):
result = a - b
print(result)

这段代码定义了两个函数,使用装饰器@logging对这两个函数进行装饰。

装饰器@logging是一个函数,用于装饰其他函数。它的作用是在被装饰的函数执行前和执行后打印一些日志信息,以便于调试和追踪代码执行情况。 

函数sum_num(args, *kwargs)是一个可变参数函数,用于计算传入的所有参数的和。它的参数列表中包括一个可变位置参数args和一个可变关键字参数kwargs。函数体中使用循环遍历args和kwargs.values(),将它们的值相加,并返回结果。 

函数subtraction(a, b)是一个简单的函数,用于计算a和b的差,并打印结果。它的函数体中只有一行代码。 

因为这两个函数都使用了装饰器@logging进行装饰,所以在它们执行前和执行后都会打印日志信息。这些日志信息包括函数名、参数列表和返回值等。这样可以方便地追踪函数的执行情况和调试代码。

 
 
 
 

result = sum_num(1, 2, a=10)
print(result)

subtraction(4, 2)

这段代码定义了两个函数sum_num()和subtraction()并调用它们。下面是对这些代码的详细解释: 

第一部分:定义函数 

1. sum_num(1, 2, a=10):定义了名为sum_num()的函数,该函数接受两个位置参数和一个名为a的关键字参数。函数的作用是返回两个位置参数的和再加上a的值。 

2. subtraction(4, 2):定义了名为subtraction()的函数,该函数接收两个位置参数,返回两个参数的差。 

第二部分:调用函数 

1. result = sum_num(1, 2, a=10):调用sum_num()函数,并将参数1和2传递给它作为位置参数,将10传递给它作为关键字参数a的值。函数执行后,返回1 + 2 + 10 = 13,然后将这个结果赋值给result变量。 

2. print(result):打印result变量的值,即13。 

3. subtraction(4, 2):调用subtraction()函数,并将参数4和2传递给它作为位置参数。函数执行后,返回4 - 2 = 2,但是这个结果没有被存储在任何变量中,也没有被打印出来。

带有参数的装饰器介绍

1、带有参数的装饰器介绍

带有参数的装饰器就是使用装饰器装饰函数的时候可以传入指定参数,语法格式: @装饰器(参数,...)

错误演示

 
 
 
 

def decorator(fn, flag):
def inner(num1, num2):
if flag == "+":
print("--正在努力加法计算--")
elif flag == "-":
print("--正在努力减法计算--")
result = fn(num1, num2)
return result
return inner

这是一个装饰器函数,接受两个参数:一个函数和一个标志。返回一个内部函数 inner。 

inner 函数根据标志 flag 的不同,输出不同的提示信息。然后调用传入的函数 fn,传入 num1 和 num2 两个参数,并返回结果。最终将结果返回给调用者。 

装饰器函数的作用是在不改变原函数代码的前提下,给函数添加额外的功能。在这个例子中,装饰器函数可以为加法和减法函数添加提示信息,让程序更加友好。

 
 
 
 

@decorator('+')
def add(a, b):
result = a + b
return result

result = add(1, 3)
print(result)

这是一个装饰器函数。装饰器函数可以添加功能或修改函数的行为,而无需更改源代码。在这里,装饰器函数用符号 '+' 将两个数相加,并返回计算结果。 

该装饰器函数被应用在 add(a, b) 函数上,它将两个参数相加并返回结果。因此,在调用 add(a, b) 函数时,实际上执行了装饰器函数中的逻辑。该函数将两个参数相加,并返回计算结果。 

最后,实例化 add(1, 3) 函数,并将结果(4)打印出来。 执行结果:

 
 
 
 

Traceback (most recent call last):
File "/home/python/Desktop/test/hho.py", line 12, in <module>
@decorator('+')
TypeError: decorator() missing 1 required positional argument: 'flag'

2、正确语法

在装饰器外面再包裹上一个函数,让最外面的函数接收参数,返回的是装饰器,因为@符号后面必须是装饰器实例。

 
 
 
 

# 添加输出日志的功能
def logging(flag):

def decorator(fn):
def inner(num1, num2):
if flag == "+":
print("--正在努力加法计算--")
elif flag == "-":
print("--正在努力减法计算--")
result = fn(num1, num2)
return result
return inner

# 返回装饰器
return decorator

这段代码定义了一个装饰器函数 logging,它接受一个字符串参数 flag,并返回一个装饰器函数 decorator。 

decorator  函数接受一个函数 fn 作为参数,它定义了一个内部函数 inner,该函数接受两个参数 num1 和 num2。在 inner 函数中,根据传入的 flag  值输出相应的日志信息,然后调用原始函数 fn 并返回其结果。 

最后,decorator 函数返回 inner 函数作为装饰器,并将其应用于被装饰的函数。这样,当被装饰的函数被调用时,会先输出相应的日志信息,然后再执行原始函数。

 
 
 
 

# 使用装饰器装饰函数
@logging("+")
def add(a, b):
result = a + b
return result

@logging("-")
def sub(a, b):
result = a - b
return result

result = add(1, 2)
print(result)
result = sub(1, 2)
print(result)

这段代码定义了两个函数add和sub,分别实现加法和减法运算。同时,使用装饰器@logging对这两个函数进行装饰,装饰器函数logging会在函数执行前和执行后打印日志信息,其中使用"+"和"-"作为日志信息的前缀。最后,分别调用add和sub函数,输出它们的返回结果。

类装饰器使用

1、类装饰器的介绍

装饰器还有一种特殊的用法就是类装饰器,就是通过定义一个类来装饰函数。

 
 
 
 

class Check(object):
def __init__(self, fn):
# 初始化操作在此完成
self.__fn = fn

# 实现__call__方法,表示把类像调用函数一样进行调用。
def __call__(self, *args, **kwargs):
# 添加装饰功能
print("请先登陆...")
self.__fn()

这段代码定义了一个装饰器类 Check,用于在被装饰的函数执行前进行一些操作,例如检查用户是否已经登录。 

在类的初始化方法 init 中,将被装饰的函数 fn 保存在实例属性 fn 中。 在类中定义了一个特殊方法 call,该方法会在类实例像函数一样被调用时执行。在 call 方法中,先输出一条提示信息,然后调用保存在 fn 属性中的函数。 

当我们使用该装饰器来装饰一个函数时,实际上是创建了一个 Check 类的实例,并将被装饰的函数作为参数传入该实例的初始化方法 init 中,然后将该实例作为函数的新定义。因此,当我们调用被装饰的函数时,实际上是调用了 Check 类的实例,进而调用了 call 方法,从而实现了装饰器的功能。

 
 
 
 

@Check
def comment():
print("发表评论")

comment()

2、代码说明

@Check 等价于 comment = Check(comment), 所以需要提供一个init方法,并多增加一个fn参数。 

要想类的实例对象能够像函数一样调用,需要在类里面使用call方法,把类的实例变成可调用对象(callable),也就是说可以像调用函数一样进行调用。 

在call方法里进行对fn函数的装饰,可以添加额外的功能。

猜你喜欢

转载自blog.csdn.net/Blue92120/article/details/131332567