Chapter 7 Function Decorators and Closures
函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。
7.1 装饰器基础知识
- 装饰器是可调用的对象,其参数是另一个函数。Python 也支持类装饰器,见第 21 章;
- 装饰器
@decorate效果与target = decorate(target)一致,可能会将其替换为新的函数。
7.2 Python 何时执行装饰器
- 函数装饰器在导入模块时立即执行,也即在运行前、导入时就已经完成了函数的更新。而不是在调用时再执行;
- 注册装饰器:将函数添加到中央注册处(如在 Web 框架中用到),可能会或不会修改函数。
7.3 使用装饰器改进“策略”模式
- 使用注册装饰器能够将策略函数直接添加到注册列表中,这样对策略函数的名称、实现、模块都没有要求,只需要标注装饰器即可。
7.4 变量作用域规则
- Python 不要求声明变量,但假定在函数定义体中赋值的变量是局部变量(包括形式参数),如果定义体中没有出现过赋值,则是全局变量。注意:如果出现了赋值,则它在整个函数中都是局部变量,不论赋值在前在后;
- 如果在赋值时要指定为全局变量,要使用
global声明; - 上述特征可以通过字节码观察:
dis.dis(func)函数可以生成函数的字节码。运行字节码的 CPython VM 是栈机器。
7.5 闭包
- 闭包是延伸了作用域的函数,能够访问不在函数体内定义的非全局变量,称作自由变量。它保留了这些自由变量的绑定,这样后续仍能够使用它们;
- 可以在所得函数的
__code__.co_freevars中检查到自由变量的存在,这些自由变量的名称对应于__closure__元组中的各cell对象,其属性cell.cell_contents保存着真正的值。
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
7.6 nonlocal 声明
- 当变量是数字或任何不可变类型时,增量赋值语句的作用相当于赋值,因此会将变量的作用域视为是局部变量(隐式创建局部变量),而非自由变量。而对列表等可变对象进行追加等操作时,并不会重新赋值,因此会视为自由变量;
- 可以使用
nonlocal声明这些要使用的不可变自由变量,这样在重新赋值时,闭包中保存的绑定会更新到新对象。
7.7 实现一个简单的装饰器
- 再次注意:添加功能后,原来函数名称所指的函数对象发生了变化;
- 使用
@functools.wraps(func)装饰新定义的函数,可以将__name__、__doc__等属性复制到新函数中,保留函数的信息; - 一般在定义函数时的参数设置为
*args, **kwargs,并将其直接传给func,保留原函数的功能。
7.8 标准库中的装饰器
备忘:functools.lru_cache
- 把耗时的函数的结果保存起来,避免传入相同的参数时重复计算(分治法中递归函数的备忘);
- 使用方式:
@functools.lru_cache(maxsize=128, typed=False),分别表示缓存中存储的结果总数(满了之后按照 LRU 的方式删除,应设为2 的幂)、是否区分不同的参数类型(如浮点数和整数参数); - 由于
lru_cache使用字典存储计算结果,函数所有的参数都必须是可散列的; - 可以叠放装饰器(装饰器的嵌套)。
单分派泛函数
- 技巧:
html.escape()可以把字符串转义为 HTML 中的文本; - Python 不支持重载方法或函数;
@functools.singledispatch装饰器能够将整体方案拆成多个模块。使用它装饰的普通函数会变成泛函数,根据第一个参数的类型,以不同方式执行相同操作;- 各个专门函数使用
@<<base_function>>.register(<<type>>)进行修饰,可叠放多个register装饰器,其名称可以使用_; - 尽可能使用虚拟超类来注册各个类型,如整数
numbers.Integral和可变序列abc.MutableSequence等,以增强可兼容性; - 为同一方法定义多个重载变体,比在一个函数中使用
if-else要更好。
7.9 叠放装饰器
7.10 参数化装饰器
- 使用装饰器工厂函数,它接受参数来创建装饰器对象,随后把这个装饰器应用到函数上;
- 在装饰器工厂函数内,实现的装饰器内部可以读取外层函数的参数(闭包);
- 参数化装饰器在结构上通常需要 3 层嵌套(工厂函数、装饰器函数、内部创建的新函数);
- 技巧:
locals()返回局部符号表字典,globals()返回当前全局符号表(当前模块)的字典。