函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。
本章的最终目标是解释清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的参数化装饰器。但是,在实现这一目标之前,我们要讨论下述话题:
- Python 如何计算装饰器句法
- Python 如何判断变量是不是局部的
- 闭包存在的原因和工作原理
- nonlocal 能解决什么问题
掌握这些基础知识后,我们可以进一步探讨装饰器:
- 实现行为良好的装饰器
- 标准库中有用的装饰器
- 实现一个参数化装饰器
装饰器的基础知识
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
@decorate
def func():
print('run func')
# 上面代码等价于
def func():
print('run func')
func = decorate(func)
两种写法的最终结果一样:上述两个代码片段执行完毕后得到的 target 不一定是原来那个 target 函数,而是 decorate(target) 返回的函数。
>>> def deco(f):
... def inner():
... print('run inner')
... return inner
>>> @deco
... def target():
... print('run target')
...
>>> target()
run inner
>>> target
<function deco.<locals>.inner at 0x000001F0CA6FC840>
>>>
# 调用被装饰的target其实运行inner
# target现在是inner的引用
严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。
综上,装饰器的俩大特性是
- 能把被装饰的函数替换成其他函数。
- 装饰器在加载模块时立即执行。
python何时执行装饰器
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时)
函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这突出了 Python 程序员所说的导入时和运行时之间的区别。
变量作用域规则
b = 1
def fun1(a):
print(a)
print(b)
b = 2
if __name__ == "__namin__":
fun1(‘a')
输出结果:
Traceback (most recent call last):
File ".\test1.py", line 7, in <module>
fun1('a')
File ".\test1.py", line 4, in fun1
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
# 使用全局变量用global申明
b = 1
def fun1(a):
global b
print(a)
print(b)
b = 2
python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。后面调用 func(3)时,func 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现b 没有绑定值。
闭包
def mack_avg():
ls = []
def avg(n):
ls.append(n)
return num(ls) / len(ls)
return avg
avg = mack_avg()
avg(10)
avg(11)
__code__ :表示编译后的函数定意体
>>> avg.__code__.co_varnames
('n',)
>>> avg.__code__.co_freevars
('ls',)
# ============================
avg.__closure__ 中的各个元素对应avg.__code__.co_freevars 中的一个名称。
这些元素是 cell 对象,有个cell_contents 属性,保存着真正值。
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
nonlocal声明
它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。这样,count 就不是自由变量了,因此不会保存在闭包中。如果使用 nonlocal 声明变量,被申明的变量赋予新值,闭包中保存的绑定会更新。