函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。

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() 返回当前全局符号表(当前模块)的字典。