深入浅出Python闭包与装饰器
闭包
闭包就是一种函数的嵌套,在一个函数中,有另一个函数的定义,并且这个里面的函数使用到了外部函数的变量,那么这个内部函数以及用到的外部函数的变量就构成了一个特殊的对象,这个特殊的对象就是闭包,或者说把这个特殊的对象当作闭包来对待。
闭包避免了使用全局变量,使得局部变量在函数外被访问成为可能,相比较面向对象,不用继承那么多的额外方法,闭包占用了更少的空间。
举一个例子:
def sum(a,b):
def third(c):
print(a+b+c)
return third
装饰器
在不修改一个原有函数内部代码的前提下,在函数的外面定义一个闭包,然后在原有函数的上一行写上"@"符号加上闭包最外层的函数名,这样可以在不修改原有函数的前提下,给原来的函数增加新的功能。(但是闭包内部的功能只能在原有函数执行前执行,或是原有函数都执行完之后执行)这就是一个装饰器。
装饰器的实现过程
首先定义一个闭包,把函数名传给这个闭包。
def set_func(func):
def call_func():
print("调用闭包")
func()
return call_func
# @set_func
def test():
print("调用函数test")
res = set_func(test)
res()
把test函数传给set_func函数,call_func函数在被调用之前指向一块区域,在set_func函数return call_func时,call_func函数指向的区域有一个print函数和func()函数,因为传递给set_func函数的参数是tets,所以这时,闭包中的func()函数就指向了test函数。
因为res得到的是set_func函数返回的return call_func
,所以res指向的内容和call_func指向的内容是一样的,即一个print函数和func()函数(test)。
res()
加上括号就是调用res指向的区域的内容,所以执行函数得到:
还可以写成这样:
def set_func(func):
def call_func():
print("调用闭包")
return func()
return call_func
def test():
print("调用函数test")
test = set_func(test)
test()
还有一个问题是,如果把res换成test,运行程序后是怎样的?
def set_func(func):
def call_func():
print("调用闭包")
func()
return call_func
# @set_func
def test():
print("调用函数test")
test = set_func(test)
test()
执行一下结果还是:
image.png
这是因为在set_func(test)的时候,已经把test传递进去,所以func函数已经拿到test函数的代码了,即print("调用函数test"),所以即使test这个变量名被赋了新的值或调用,func还是可以执行原来test中的代码,即print("调用函数test")。
传递test的时候:
image.png
test有了新的指向后:
image.png
所以两个红色框是等价的:
image.png
对有参数,无返回值的函数进行装饰
修改test,改为有参函数:
def test(num):
print("调用函数test")
print("数字为:", num)
image.png
image.png
修改代码后:
def set_func(func):
def call_func(arguments):
print("调用闭包")
return func(arguments)
return call_func
@set_func
def test(num):
print("调用函数test")
print("数字为:", num)
# test = set_func(test)
test(100)
这时test传递进来的100会传递给call_func作为arguments,所以再次执行程序:
image.png
在调用函数之前,装饰器语句就已经被执行了
Python解释器在解释的时候,当遇到函数的定义,先不执行,遇到函数的调用会执行,而遇到装饰器语句,即@开头的语句,无论函数有没有被调用,装饰器语句都会被执行。
修改代码如下:
def set_func(func):
print("开始装饰")
def call_func(arguments):
print("调用闭包")
return func(arguments)
return call_func
@set_func
def test(num):
print("调用函数test")
print("数字为:", num)
# test = set_func(test)
# test(100)
test函数的调用语句已经被注释掉了,现在来执行程序:
image.png
对不定长参数的函数进行装饰
修改test函数:
@set_func
def test(num, *args, **kwargs):
print("调用函数test")
print("数字为:", num)
print("*args:", args)
print("**kwargs:", kwargs)
当调用test只传递一个值时:
def set_func(func):
print("开始装饰")
def call_func(arguments):
print("调用闭包")
return func(arguments)
return call_func
@set_func
def test(num, *args, **kwargs):
print("调用函数test")
print("数字为:", num)
print("*args:", args)
print("**kwargs:", kwargs)
# test = set_func(test)
test(100)
执行结果:
但是当传入的参数为多个时,执行程序会出错:
调用:
test(100,200)
image.png
所以将程序修改为:
def set_func(func):
print("开始装饰")
def call_func(*args, **kwargs):
print("调用闭包")
return func(*args, **kwargs)
return call_func
@set_func
def test(num, *args, **kwargs):
print("调用函数test")
print("数字为:", num)
print("*args:", args)
print("**kwargs:", kwargs)
# test = set_func(test)
test(100,200)
再次执行:
image.png
在传参数的过程中,参数如果传给装饰器的也要如何传给函数,所以在func中也要有星号"*":
image.png
如果没有星号,写成'func(args, kwargs)'的话,相当于给test函数传递了两个参数,一个元组,一个字典。
对有返回值的函数进行装饰
其实前面的代码已经有return了:
image.png
这个return对没有返回值的函数不影响,如果没有返回值,这个return会返回None,不影响test函数的调用。
通用模板
所以一个通用的装饰器模板如下:
def set_func(func):
"""代码"""
def call_func(*args, **kwargs):
"""代码"""
return func(*args, **kwargs)
return call_func
多个装饰器装饰一个函数
代码如下:
def set_func1(func):
print("开始装饰,这是装饰器1")
def call_func(*args, **kwargs):
print("调用闭包1")
return func(*args, **kwargs)
print("***********")
return call_func
def set_func2(func):
print("开始装饰,这是装饰器2")
def call_func(*args, **kwargs):
print("调用闭包2")
return func(*args, **kwargs)
print("***********")
return call_func
def set_func3(func):
print("开始装饰,这是装饰器3")
def call_func(*args, **kwargs):
print("调用闭包3")
return func(*args, **kwargs)
print("***********")
return call_func
@set_func1
@set_func2
@set_func3
def test(*args, **kwargs):
print("调用函数test")
print("*args:", args)
print("**kwargs:", kwargs)
return "ok"
# test = set_func(test)
test(100,200)
执行结果:
image.png
由执行结果可以看到,装饰器先执行的装饰器3——>2——>1,闭包的执行顺序是闭包1——>2——>3。
多个装饰器装饰一个函数的时候,先装的是下面的装饰器,可以简单理解为,因为装饰器装饰的是函数,如果第一个装饰的是第一个装饰器的话,他下面还不是函数,所以不能先装饰上面的装饰器。
未开始装饰之前:
执行到这里:
image.png
因为test函数作为func已经传递给set_func3,所以set_func3中的func指向了test指向的区域。
image.png
因为@set_func3等价于
test = set_func3(test)
,所以当set_func3(test)赋给test后,test这个变量名就不再指向原来的区域了,而是指向set_func3(test)的返回,即return call_func
,所以test变量名指向了call_func指向的区域。image.png
这样set_func3的装饰就结束了,开始set_func2的装饰。
set_func2一开始指向的是test指向的区域,因为@set_func2等价于
test = set_func2(test)
。image.png
也因为@set_func2等价于
test = set_func2(test)
,所以test变量名被赋予新的值之后:image.png
同理set_func1:
image.png
所以在test被调用之前:
装饰器3先装饰:
image.png
然后是装饰器2:
image.png
然后是装饰器1:
image.png
之后因为语句
test(100,200)
,test被调用,所以依照以下的顺序执行:image.png
即:
image.png