Python的装饰器是一大利器,几乎所有的流行框架,都在广泛使用装饰器
为什么说没有闭包( closure) ,装饰器的功能会大打折扣?
一个最简易的装饰器是什么都不不做,只是透传一个函数对象
如
def final(f):
return f
装饰器的原理是
@deco
def func():
....
func() # call
等价于
def func():
...
func = deco(func) # 注意这里
func()
func = deco(func) 右边的表达式有可能偷梁换柱把函数换成了另外一个函数,有可能没有换,只是在对func对象做了一些事情,比如加一点属性之类的,也可以只是在func调用之前增加一些别的事情。
deco(func) 内部做什么,灵活性极大
这也是装饰器的特点之一——不需要修改func的定义和实现,既可以灵活地操纵它的调用行为,借用动态语言的特性,既可以动态注入属性,也可以添加想要的逻辑,很灵活。
更多的一种装饰器是——偷梁换柱。
即经过修饰之后函数已经不是原来那个。可能在装饰器器内部已经计算出结果,同时转换成另外一个函数。
这个“另外”的函数通常是通过闭包来提供的。
def deco(func):
def wrapper():
ret = func()
return ret
return wrapper
闭包在python中有一个 __closure__ 协议
代码
def advance_avg():
ls = [] # 只初始化一次
s = 1
def wrapper(num):
nonlocal s # 必须要声明 nolocal 否则会报告未知属性错误
s += num
print(f"s is {s}")
ls.append(num)
total = sum(ls)
return total / len(ls)
return wrapper
当调用一次 advance_avg() 的时候,s和ls都会绑定在对象上
adv = advance_avg() # int 和 list对象都已经绑定
以后每次调用adv(val) 绑定的ls和s都可以被内部的函数wrapper操作。而且一大特性是这两个变量可以缓存上一次调用之后计算的结果,且看
adv = advance_avg()
adv(1) # 结果是1
adv(2) # 结果是1.5 不是2
adv.__closure__ # 输出 <cell at 0x7f8da8143520: list object at 0x7f8d9801db40>, <cell at 0x7f8da8143910: int object at 0x108f92ae0>
adv的 __closure__ 属性输出了一个int 和 list对象地址,这两个对象的生命周期跟随adv 。
理解闭包关键的地方在于作用域的概念。
有个可能不太精确的类比
对于嵌套闭包,我们可以把最上层的函数定义视为一个类定义,在python 中一切皆对象,包括函数
对于类的写法,我们同样可以写一个上述用起来一模一样的功能类,只是需要在类内部定义一下__call__ —— 大佬推荐的工程级别的装饰器写法,用定义了__call__ 函数的类
class Average:
def __init__(self):
self.series = []
self.s = 0
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
这个类用起来跟上面的闭包函数形式上没有太大的区别。
__init__ 函数有一次性声明并且追随Average对象声明周期的两个变量,这个和闭包一样,带闭包的 advance_avg函数对象保持着一个int和一个list的生命周期
总结
- 类的方法,可以访问类内的self属性
- 方法中的方法(闭包)可以访问上层方法定义的局部属性(类比与类内定义的self 方法)
- 函数可以多层嵌套,访问范围是内层有外层的访问权,但是反过来不行——否则封装毫无意义
- 闭包访问上层局部变量,如果变量是不可变对象,一定要使用 nolocal声明,否则会报告未知属性错误