9. 第九章 装饰器和闭包

发布时间:2024年01月22日

9. 装饰器和闭包

有很多人抱怨, 把这个功能命名为'装饰器'不好.
主要原因是, 这个名称与<<设计模式: 可复用面向对象软件的基础>> (后面简称<<设计模式>>)中使用的不一致.
装饰器这个名称可能更适合在编译器领域使用, 因为它会遍历并注解句法树.
                                            -- 'PEP 318-Decorators for Functions and Methods'
函数装饰器允许在源码中'标记'函数, 以某种方式增强函数的行为.
这是一个强大的功能, 但是如果想掌握, 则必须理解闭包, 即捕获函数主体外部定义的变量.

Python 3.0引入的保留关键字nonlocal鲜为人知.
作为Python程序员, 如果严格遵守基于类的面向对象编程方式, 那么即便不知道它的存在也不受影响.
然而, 如果想自己实现函数装饰器, 则必须了解闭包的方方面面, 因此也就需要掌握nonlocal.

除了在装饰器中有用, 闭包还是回调式编程和函数式编程风格的重要基础.

本章的最终目标是解释清楚函数装饰器的工作原理, 从最简单的注册装饰器开始,
一直讲到较为复杂的参数化装饰器.
然而, 在实现这一目标之前, 需要讨论以下话题:
? Python如何求解装饰器句法; 
? Python如何判断变量是不是局部的;
? 闭包存在的原因和工作原理;
? nonlocal能解决什么问题.

掌握这些基础知识后,可以再进一步探讨装饰器:
? 实现行为良好的装饰器:
? 标准库中强大的装饰器: @cache, @lru_cache和@singledispatch, 
? 实现一个参数化装饰器.

9.1 本章新增内容
Python 3.9新引入的缓存装饰器functools.cache比传统的functools.lru_cache更简单,
因此本章会先对其进行讲述. functools.lru_cache和Python 3.8新增的简化版将在9.9.2节讨论.

9.9.3节内容有扩充, 增加了类型提示, 自Python 3.7开始, 这是functools.singledispatch的推荐用法.

9.10节增加了基于类的示例, 即示例9-27.

我调整了全书结构, 把第1版的第6(现第10)移到了第二部分末尾.
1版中的7.3节也移到了第10, 与使用可调用对象实现的其他策略设计模式放在一起.

下面先简要介绍装饰器的基础知识, 然后再讨论本章开篇列出的各个话题.
9.2 装饰器基础知识
装饰器是一种可调用对象, 其参数是另一个函数(被装饰的函数).

装饰器可能会对被装饰的函数做些处理, 然后返回函数, 或者把函数替换成另一个函数或可调用对象. 
(1: 把这句话中的'函数'换成'类', 差不多就是类装饰器的作用. 24章将讨论类装饰器. )

也就是说, 假如有一个名为decorate的装饰器:
@decorate
    def target():
        print('running target()')
        
那么上述代码的效果与下述写法一样.
def target():
    print('running target()')
    
target = decorate(target)

两种写法的最终结果一样: 上述两个代码片段执行完毕后, 
target名称都会绑定decorate(target)返回的函数--可能是原来那个名为target的函数, 也可能是另一个函数.

为了确认被装饰的函数被替换了, 请看示例9-1中的控制台会话.
# 示例 9-1 装饰器通常会把一个函数替换成另一个函数
>>> def deco(func):
...     def inner():
...         print('running inner()')
        # deco返回内部的函数对象inner.
...     return inner  
...

# 使用deco装饰target.
>>> @deco  
... def target():
...     print('running target()')
...

# 调用被装饰的target, 运行的其实是inner.
>>> target()
running inner()

# 查看对象, 发现target现在是inner的引用.
>>> target
<function deco.<locals>.inner at 0x10063b598>

严格来说, 装饰器只是语法糖.
如前所述, 装饰器可以像常规的可调用对象那样调用, 传入另一个函数.
有时, 这样做其实更方便, 尤其是做元编程(在运行时改变程序的行为).

综上所述, 装饰器有以下3个基本性质.
? 装饰器是一个函数或其他可调用对象.
? 装饰器可以把被装饰的函数替换成别的函数. 
? 装饰器在加载模块时立即执行.
下面重点讲解第3.
9.3 Python何时执行装饰器
装饰器的一个关键性质是, 它们在被装饰的函数定义之后立即运行.
这通常是在导入时(例如, 当Python加载模块时).
以示例9-2中的registration.py模块为例.
# 示例9-2 registration.py 模块

# registry保存被@register装饰的函数引用.
registry = []


# register的参数是一个函数.
def register(func):
    # 为了演示, 显示被装饰的函数.
    print(f'running register({func})')
    # 把func存入registry.
    registry.append(func)
    # 返回func: 必须返回函数, 这里返回的函数与通过参数传入的函数一样.
    return func


# 使用@register装饰f1和f2.
@register
def f1():
    print('running f1()')


@register
def f2():
    print('running f2()')


# 没有装饰f3.
def f3():
    print('running f3()')


# main首先显示registry, 然后调用f1(), f2()和f3().
def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()


# 只有把registration.py 当作脚本运行时才调用main().
if __name__ == '__main__':
    main()

把registration.py当作脚本运行, 得到的输出如下所示.
$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at Ox100631bf8>, <function f2 at Ox100631c80>]
running f1()
running f2()
running f3()

注意, register在模块中其他函数之前运行(两次) . 
调用register时, 传给它的参数是被装饰的函数, 例如<function f1 at 0x100631bf8>.

加载模块后, registry中有两个被装饰函数(F1和f2)的引用.
这两个函数, 以及f3, 只在main显式调用它们时才执行。

如果是导入registration.py模块(不作为脚本运行)则输出如下所示.
>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)

这种情况下, 如果查看registry的值, 则得到的输出如下所示.
>>> registration.registry
[<function f1 at Ox10063b1e0>, <function f2 at 0x10063b268>]

示例9-2主要想强调, 函数装饰器在导入模块时立即执行, 而被装饰的函数只在显式调用时运行.
由此可以看出Python程序员所说的导入时和运行时之间有什么区别.
9.4 注册装饰器
考虑到装饰器在真实代码中的常用方式, 示例9-2有两处不寻常的地方.
? 装饰器函数与被装饰的函数在同一个模块中定义.
  实际情况是, 装饰器通常在一个模块中定义, 然后再应用到其他模块中的函数上.
? register装饰器返回的函数与通过参数传入的函数相同.
  实际上, 大多数装饰器会在内部定义一个函数, 然后将其返回.
  
虽然示例9-2中的register装饰器原封不动地返回了被装饰的函数, 但是这种技术并非毫无用处.
很多Python框架会使用这样的装饰器把函数添加到某种中央注册处,
例如把URL模式映射到生成HTTP响应的函数的注册处.
这种注册装饰器可能会也可能不会更改被装饰的函数.

不过, 大多数装饰器会更改被装饰的函数.
通常的做法是, 返回在装饰器内部定义的函数, 取代被装饰的函数.
涉及内部函数的代码基本上离不开闭包. 为了理解闭包, 需要后退一步, 先研究Python中的变量作用域规则.
9.5 变量作用域规则
示例9-3定义并测试了一个函数, 该函数会读取两个变量的值:
一个是通过函数的参数传入的局部变量a, 另一个是函数没有定义的变量b.
# 示例9-3 一个函数, 该函数会读取一个局部变量和一个全局变量
>>> def f1(a):
...     print(a)
...     print(b)

>>> f1(3)
3
Traceback (most recent call last):
  File "stdin>", line 1, in <module>
  File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined
# 我的编译器给出: NameError: name 'b' is not defined, 没有提示global.

出现错误并不奇怪. 接着示例9-3. 如果先给全局变量b赋值, 然后再调用f1, 则不会出错.
>>> b = 6
>>> f1(3)
3 
6

下面来看一个可能会让你吃惊的示例.

看一下示例9-4中的f2函数. 
前两行代码与示例9-3中的f1一样, 然后为b赋值. 可是, 赋值前面那个print失败了.
# 示例9-4 b是局部变量, 因为在函数主体中给它赋值了
>>> b = 6  
>>> def f2(a):
...    print(a)
...    print(b)  
...    b = 9
...

>>> f2(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
UnboundLocalError: local variable 'b' referenced before assignment
# 未绑定的局部误差: 局部变量'b'引用之前的任务

注意, 首先输出的是3, 这表明执行了print(a)语句.
但是, 第二个语句, 即print(b), 绝对不会执行.
一开始我很吃惊, 觉得会打印6, 因为有一个全局变量b, 而且局部变量b是在print(b)之后才赋值的.

可事实是, Python编译函数主体时, 判断b是局部变量, 因为在函数内给它赋值了. 
生成的字节码证实了这种判断. 所以, Python会尝试从局部作用域中获取b.
后面调用f2(3), f2的主体顺利获取并打印局部变量a的值, 但是在尝试获取局部变量b的值时, 发现b没有绑定值.

这不是bug, 而是一种设计选择: Python不要求声明变量, 但是会假定在函数主体中赋值的变量是局部变量.
这比JavaScript的行为好多了, JavaScript也不要求声明变量, 
但是如果忘记把变量声明为局部变量(使用var), 则可能会在不知情的情况下破坏全局变量.

在函数中赋值时, 如果想让解释器把b当成全局变量, 为它分配一个新值, 就要使用global声明.
>>> b = 6
>>> def f3(a):
...	     global b
...		 print(a)
...		 print(b)
...		 b = 9

>>> f3(3)
3
6
>>> b
9

通过以上示例可以发现两种作用域.
模块全局作用域
   在类或函数块外部分配值的名称.
f3函数局部作用城
  通过参数或者在函数主体中直接分配值的名称.
  
变量还有可能出现在第3个作用域中, 我们称之为'非局部'作用域.
这个作用域是闭包的基础, 稍后详述.
了解Python的变量作用域之后, 9.6节可以讨论闭包了.
如果好奇示例9-3和示例9-4中两个函数生成的字节码有什么区别, 请阅读以下附注栏中的内容。
*---------------------------------------------比较字节码--------------------------------------*
dis模块为反汇编Python函数字节码提供了简单的方式.
示例9-5和示例9-6分别是示例9-3中的f1和示例9-4中的f2的字节码.
# 示例9-5反汇编示例9-3中的f1函数
>>> from dis import dis
>>> dis(f1)
  2           0 LOAD_GLOBAL                0 (print)  # 加载全局名称print.
              3 LOAD FAST                  0 (a)  # 加载局部名称a.
              6 CALL_FUNCTION              1 (1 positional, 0 keyword pair)
              9 POP_TOP
              
  3          10 LOAD_GLOBAL                0 (print)
             13 LOAD_GLOBAL                1 (b)  # 加载全局名称b.
             16 CALL_FUNCTION              1 (1 positional, θ keyword pair)
             19 POP_TOP
             20 LOAD_CONST                 0 (None)
             23 RETURN_VALUE
            
请比较示例9-5中f1的字节码和示例9-6中f2的字节码.
# 示例9-6 反汇编示例9-4中的f2函数
>>> dis(f2)
  2          0 LOAD_GLOBAL                  0 (print)
             3 LOAD_FAST                    0 (a)
             6 CALL_FUNCTION                1 (1 positional, 0 keyword pair)
             9 POP_TOP
            
  3         10 LOAD_GLOBAL                  0 (print)
            13 LOAD_FAST                    1 (b)  # <1>
            16 CALL_FUNCTION                1 (1 positional, 0 keyword pair)
            19 POP_TOP

  4         20 LOAD_CONST                   1 (9)
            23 STORE_FAST                   1 (b)
            26 LOAD_CONST                   0 (None)
            29 RETURN_VALUE
<1>加载局部名称b. 
这表明, 虽然在后面才为b赋值, 但是编译器会把b视作局部变量,
因为变量的种类(是不是局部变量)在函数主体中不能改变.

运行字节码的CPython虚拟机(virtual machine, VM)是栈机器, 因此LOAD操作和POP操作引用的是栈.
深入说明Python操作码不在本书范畴之内, 不过dis模块文档有说明.
*--------------------------------------------------------------------------------------------*
9.6 闭包
在博客圈, 人们有时会把闭包和匿名函数弄混.
这是有历史原因的: 在函数内部定义函数以前并不常见, 也不容易, 直到出现匿名函数.
而且, 只有涉及嵌套函数时才有闭包问题.
因此, 很多人是同时知道这两个概念的.

其实, 闭包就是延伸了作用域的函数, 包括函数(姑且叫f吧)主体中引用的非全局变量和局部变量.
这些变量必须来自包含f的外部函数的局部作用域.

函数是不是匿名的没有关系, 关键是它能访问主体之外定义的非全局变量.

这个概念难以掌握, 最好通过示例理解.

假如有个名为avg的函数, 它的作用是计算不断增加的系列值的平均值, 
例如, 计算整个历史中某个商品的平均收盘价.
新价格每天都在增加, 因此计算平均值时要考虑到目前为止的所有价格.

先来看avg函数的用法.
>>> avg(10)
10.0
>>> avg(11)
10.5
>>>avg(12)
11.0

avg从何而来, 它又在哪里保存历史值呢?

初学者可能会像示例9-7那样使用基于类的实现.
# 示例9-7 average_oo.py: 一个计算累计平均值的类
class Averager():
	def __init__(self):
		self.series = []

	def __call__(self, new_value):
		# 将参数添加到列表
		self.series.append(new_value)
		# 统计列表中数值的总数
		total = sum(self.series)
		# 返回平均值, 总数 / 元素个数
		return total / len(self.series)

Averager 类的实例是可调用对象.
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

示例9-8是函数式实现, 使用了高阶函数make_averager.
# 示例9-8 average.py: 一个计算累计平均值的高阶函数

def make_averager():
	series = []

	def averager(new_value):
		# 使用外部空间的变量
		series.append(new_value)
		total = sum(series)
		return total / len(series)

	return averager

调用make_averager, 返回一个averager函数对象.
每次调用, averager都会把参数添加到系列值中, 然后计算当前平均值, 如示例9-9所示.
# 示例9-9 测试示例9-8
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(15)
12.0

注意, 这两个示例有相似之处: 调用Averager()或make_averager()得到一个可调用对象avg,
它会更新历史值, 然后计算当前平均值. 
在示例9-7, avg是Averager类的实例;
在示例9-8, avg是内部函数averager.
不管怎样, 只需调用avg(n), 把n放入系列值中, 然后重新计算平均值即可.

作为Averager类的实例, avg在哪里存储历史值很明显: 实例属性self.series.
但是, 第二个示例中的avg函数在哪里寻找series呢?

注意, series是make_averager函数的局部变量, 因为赋值语句series = []在make_averager函数的主体中.
但是, 调用avg(10), make_averager函数已经返回, 局部作用域早就'烟消云散'.

如图9-1所示, 在averager函数中, series是自由变量(free variable).
自由变量是一个术语, 指未在局部作用域中绑定的变量.

图9-1

9-1: averager函数的闭包延伸到自身的作用域之外, 包含自由变量series的绑定
查看返回的averager对象, 
我们发现Python在__code__属性(表示编译后的函数主体)中保存局部变量和自由变量的名称, 如示例9-10所示.
# 示例9-10 查看make_averager(参见示例9-8)创建的函数
>>> avg.__code__.co_varnames  # 局部变量的名称(接收的参数也算!)
('new_value','total')
>>> avg.__code__.co_freevars  # 自由变量的名称
('series',)

series的值在返回的avg函数的__closure__属性中.
avg.__closure__中的各项对应avg.__code_.co_freevars中的一个名称.
这些项是cell对象, 有一个名为cell_contents的属性, 保存着真正的值.
这些属性的值如示例9-11所示。
# 示例9-11 接续示例9-9
>>> avg.__code__.co_freevars  # 自由变量的名称.
('series',)
>>> avg.__closure__  # 自由变量中对应的值, 是一个列表对象.
(<cell at Ox107a44f78: list object at 0x107a91a48>,)  # 按顺序一个自由变量对应一个列表对象.
>>> avg.__closure__[0].cell_contents  # 获取series对应的列表对象, 使用cell_contents展示.
[10, 11, 12]

综上所述, 闭包是一个函数, 它保留了定义函数时存在的自由变量的绑定.
如此一来, 调用函数时, 虽然定义作用域不可用了, 但是仍能使用那些绑定.
注意, 只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量.
这些外部变量位于外层函数的局部作用域内.
9.7 nonlocal声明
前面实现make_averager函数的方法效率不高.
在示例9-8, 我们把所有值存储在历史数列中, 然后在每次调用averager时使用sum求和.
更好的实现方式是, 只存储目前的总值和项数, 根据这两个数计算平均值.
示例9-12中的实现有缺陷, 只是为了阐明观点. 你能看出缺陷在哪里吗?
# 示例9-12一个计算累计平均值的高阶函数, 不保存所有历史值, 但有缺陷
def make_averager():
	count = 0
	total = 0

	def averager(new_value):
		count += 1
		total += new_value
		return total / count

	return averager

使用示例9-12中定义的函数, 结果如下所示.
>>> avg = make_averager()
>>> avg(10) 
Traceback (most recent call last):
  ...
UnboundLocalError: local variable 'count' referenced before assignment
# 未绑定本地错误: 赋值前引用了本地变量'count'

问题是, 对于数值或任何不可变类型, count += 1语句的作用其实与count = count + 3一样.
因此, 实际上我们在averager的主体中为count赋值了, 这会把count变成局部变量.
total变量也受这个问题影响.

示例9-8则没有这个问题, 因为没有给series赋值, 只是调用series.append, 并把它传给sum和len.
也就是说, 我们利用了'列表是可变对象'这一事实.

但是, 数值, 字符串, 元组等不可变类型只能读取, 不能更新.
如果像 count = count + 1这样尝试重新绑定, 则会隐式创建局部变量count.
如此一来, count就不是自由变量了, 因此不会保存到闭包中.

为了解决这个问题, Python 3引入了nonlocal关键字.
它的作用是把变量标记为自由变量, 即便在函数中为变量赋予了新值.
如果为nonlocal声明的变量赋予新值, 那么闭包中保存的绑定也会随之更新.
最新版make_averager的正确实现如示例9-13所示.
# 示例9-13 计算累计平均值, 不保存所有历史(使用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

学会使用nonlocal之后, 接下来让我们总结一下Python查找变量的方式.
9.7.1 变量查找逻辑
Python字节码编译器根据以下规则获取函数主体中出现的变量×. 
? 如果是global ×声明, 则×来自模块全局作用域, 并赋予那个作用域中×的值. 
? 如果是nonlocal x声明, 则x来自最近一个定义它的外层函数, 并赋予那个函数中局部变量×的值.
? 如果×是参数, 或者在函数主体中赋了值, 那么×就是局部变量。
? 如果引用了x, 但是没有赋值也不是参数,则遵循以下规则.
  - 在外层函数主体的局部作用域(非局部作用域)内查找×.
  - 如果在外层作用域内未找到, 则从模块全局作用域内读取.
  - 如果在模块全局作用域内未找到, 则从__builtins__.__dict__中读取.

(2: 感谢技术审校Leonardo Rochael建议做个总结.)
(3: Python没有程序全局作用域, 只有模块全局作用域.)
在对Python闭包有了一定的认识后, 下面可以使用嵌套函数着手实现装饰器了.
9.8 实现一个简单的装饰器
示例9-14定义了一个装饰器, 
该装饰器会在每次调用被装饰的函数时计时, 把运行时间, 传入的参数和调用的结果打印出来.
# 示例9-14 clockdeco0.py 一个会显示函数运行时间的简单的装饰器
import time


def clock(func):
	# 定义内部函数clocked, 它接受任意个位置参数.
	def clocked(*args):
		# 理解为计算机启动时间把, 单位为秒.
		to = time.perf_counter()
		# 这行代码行之有效, 因为clocked的闭包中包含自由变量func.
		result = func(*args)
		# 计算时间差
		elapsed = time.perf_counter() - to
		# 获取函数的名称
		name = func._name__
		# 遍历位置参数, 以逗号为间隔拼接成字符串. (只有一个元素, 做不了拼接, 不会显示逗号)
		arg_str = ','.join(repr(arg) for arg in args)
		# 打印 运行时间  函数名 位置参数 返回值
		print(f'[{elapsed:0.8f}s] {name} ({arg_str}) -> {result!r}')
		return result
    
	# 返回内部函数, 取代被装饰的函数.
	return clocked

示例9-15演示了clock装饰器的用法.
# 示例9-15 clockdeco_demo.py 使用clock 装饰器
import time	
from clockdecoo import clock


@clock
def snooze(seconds):
	time.sleep(seconds)


@clock
def factorial(n):
	# 这个的factorial是clocked函数. 
	return 1 if n < 2 else n * factorial(n - 1)


if __name__ == '__main__':
	print('*' * 40, 'Calling snooze(.123)')
	snooze(.123)
	print('*' * 40, 'Calling factorial(6)')
	print('6! =', factorial(6))

运行示例9-15, 输出如下所示.
$ python3 clockdeco_demo.py
**************************************** Calling snooze(.123)
[0.13153620s] snooze (0.123) -> None
**************************************** Calling factorial(6)
[0.00000260s] factorial (1) -> 1
[0.00002950s] factorial (2) -> 2
[0.00004730s] factorial (3) -> 6
[0.00006340s] factorial (4) -> 24
[0.00007970s] factorial (5) -> 120
[0.00010050s] factorial (6) -> 720
6! = 720

9.8.1 工作原理
如前所述, 以下内容:
@clock
def factorial(n):
	return 1 if n < 2 else n * factorial(n - 1)

其实等价于以下内容.
def factorial(n):
	return 1 if n < 2 else n * factorial(n - 1)
	
factorial = clock(factorial)

也就是说, 在这两种情况下, factorial函数都作为func参数传给clock函数(参见示例9-14),
clock函数返回clocked函数, 然后Python解释器把clocked赋值给factorial(前一种情况是在背后赋值).
导入clockdeco_demo模块, 查看factorial的__name__属性, 会看到如下结果.
>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'

可见, 现在factorial保存的其实是clocked函数的引用.
自此之后, 每次调用factorial(n)执行的都是clocked(n).
clocked大致做了下面几件事.

1. 记录初始时间t0.
2. 调用原来的factorial函数, 保存结果.
3. 计算运行时间.
4. 格式化收集的数据, 然后打印出来.
5. 返回第2步保存的结果.

这是装饰器的典型行为: 把被装饰的函数替换成新函数, 新函数接受的参数与被装饰的函数一样,
而且(通常)回返回被装饰的函数本该返回的值, 同时还会做一些额外操作.
*---------------------------------------------------------------------------------------------*
Gamma等人所著的<<设计模式>>一书是这样概述装饰器模式的: '动态地给一个对象添加一些额外的职责.'
函数装饰器符合这种说法. 但是, 从实现层面上看, Python装饰器与该书中所说的装饰器没有多少相似之处.
本章最后的'杂谈'部分会进一步探讨这个话题.
*---------------------------------------------------------------------------------------------*
示例9-14实现的clock装饰器有几个缺点:
不支持关键字参数, 而且遮盖了被装饰函数的__name__属性和__doc__属性.
示例9-16使用functools.wraps装饰器把相关的属性从func身上复制到了clocked中.
此外, 这个新版还能正确处理关键字参数.
# 示例9-16 clockdeco.py 改进后的clock装饰器
import time
import functools


def clock(func):
	@functools.wraps(func)
	def clocked(*args, **kwargs):
		t0 = time.perf_counter()
		result = func(*args, **kwargs)
		elapsed = time.perf_counter() - t0
		name = func.__name__
		arg_lst = [repr(arg) for arg in args]
		arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
		arg_str = ','.join(arg_lst)
		print(f'[{elapsed:0.8f}s] {name}({arg_str})-> {result!r}')
		return result

	return clocked

functools.wraps只是标准库中开箱即用的装饰器之一.
9.9节将介绍functools模块中最让人印象深刻的装饰器, 即cache.
9.9 标准库中的装饰器
Python 内置了3个用于装饰方法的函数: property, classmethod和staticmethod.
property将在22.4节讨论, classmethod和staticmethod将在11.5节讨论.

示例9-16用到了另一个重要的装饰器, 即functools.wraps. 它的作用是协助构建行为良好的装饰器.
标准库中最吸引人的几个装饰器, 即cache, lru_cache和singledispatch, 均来自functools模块.
下面会分别介绍它们.
9.9.1 使用functools.cache做备忘
functools.cache装饰器实现了'备忘'(memoization). 
这是一项优化技术, 能把耗时的函数得到的结果保存起来, 避免传入相同的参数时重复计算.

(4: 注意, 这个词的英文没有拼错.
memoization是一个计算机科学术语, 'memorization'(记住)有那么一点儿关系, 但不是同一个概念.)
*---------------------------------------------------------------------------------------------*
functools.cache 是Python 3.9新增的.
如果想使用Python 3.8运行本节的示例, 请把@cache换成@lru_cache.
对于更早的Python版本, 必须调用装饰器, 写成@lru_cache() (详见9.9.2).
*---------------------------------------------------------------------------------------------*
生成第n个斐波那契数这种慢速递归函数适合使用@cache, 如示例9-17所示.
# 示例9-17 生成第n个斐波那契数, 递归方式非常耗时
from clockdeco import clock


@clock
def fibonacci(n):
	if n < 2:
		return n
	return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
	print(fibonacci(6))

运行fibo_demo.py的结果如下所示.
除了最后一行, 其他输出都是clock装饰器生成的.
$ python3 fibo_demo.py
[0.00000042s] fibonacci(0)-> 0
[0.00000049s] fibonacci(1)-> 1
[0.00006115s] fibonacci(2)-> 1
[0.00000031s] fibonacci(1)-> 1
[0.00000035s] fibonacci(0)-> 0
[0.00000030s] fibonacci(1)-> 1
[0.00001084s] fibonacci(2)-> 1
[0.00002074s] fibonacci(3)-> 2
[0.00009189s] fibonacci(4)-> 3
[0.00000029s] fibonacci(1)-> 1
[0.00000027s] fibonacci(0)-> 0
[0.00000029s] fibonacci(1)-> 1
[0.00000959s] fibonacci(2)-> 1
[0.00001905s] fibonacci(3)-> 2
[0.00000026s] fibonacci(0)-> 0
[0.00000029s] fibonacci(1)-> 1
[0.00000997s] fibonacci(2)-> 1
[0.00000028s] fibonacci(1)-> 1
[0.00000030s] fibonacci(0)-> 0
[0.00000031s] fibonacci(1)-> 1
[0.00001019s] fibonacci(2)-> 1
[0.00001967s] fibonacci(3)-> 2
[0.00003876s] fibonacci(4)-> 3
[0.00006670s] fibonacci(5)-> 5
[0.00016852s] fibonacci(6)-> 8
8

浪费时间的地方很明显: fibonacci(1)调用了8, fibonacci(2)调用了5......
但是, 如果增加两行代码, 使用cache, 那么性能将显著改善, 如示例9-18所示.
# 示例9-18 使用缓存实现, 速度更快
import functools

from clockdeco import clock

# 这一行可在Python3.9或以上版本中使用. 支持更早的Python版本的做法见9.9.2节.
@functools.cache  
@clock  # 这里叠放了装饰器: @cache应用到@clock返回的函数上.
def fibonacci(n):
	if n < 2:
		return n
	return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
	print(fibonacci(6))

*---------------------------------------------------------------------------------------------*
叠放装饰器
如果想理解叠放装饰器, 那么需要记住一点: @是一种语法糖, 其作用是把装饰器函数应用到下方的函数上.
多个装饰器的行为就像调用嵌套函数一样. 
以下内容:
    @alpha
    @beta
    def my_fn():
        ...
等同于以下内容.
my_fn = alpha(beta(my_fn))
也就是说, 首先应用beta装饰器, 然后再把返回的函数传给alpha.
*---------------------------------------------------------------------------------------------*
这样一来, 对于每个n值, fibonacci函数只被调用一次.
$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8

如果要计算fibonacci(30), 使用示例9-18中的版本, 总计会调用31, 耗时0.00017,
而示例9-17中未做缓存的版本在配有Intel Core i7处理器的笔记本计算机中则耗时12.09,
因为仅fibonacci(1)就要调用832040, 总计调用2692537.

被装饰的函数所接受的参数必须可哈希, 因为底层lru_cache使用dict存储结果, 
字典的键取自传入的位置参数和关键字参数.

除了优化递归算法, @cache在从远程API中获取信息的应用程序中也能发挥巨大作用.
如果缓存较大, 则functools.cache有可能耗尽所有可用内存.
在我看来, @cache更适合短期运行的命令行脚本使用.
对于长期运行的进程, 推荐使用functools.lru_cache, 并合理设置maxsize参数(详见9.9.2).
9.9.2 使用lru_cache]
functools.cache装饰器只是对较旧的functools.lru_cache函数的简单包装.
其实, functools.lru_cache更灵活, 而且兼容Python3.8及之前的版本.

@lru_cache的主要优势是可以通过maxsize参数限制内存用量上限.
maxsize参数的默认值相当保守, 只有128, 即缓存最多只能有128.
LRU是'Least Recently Used'的首字母缩写, 表示一段时间不用的缓存条目会被丢弃, 为新条目腾出空间.

从Python3.8开始, lru_cache 有两种使用方式.
下面是最简单的方式.
@lru_cache
def costly_function(a, b):

另一种方式是从Python3.2开始支持的加上()作为函数调用.
@lru_cache()
def costly_function(a, b):
    
两种用法都采用以下默认参数.

maxsize=128
  设定最多可以存储多少条目. 
  缓存满了之后, 最不常用的条目会被丢弃, 为新条目腾出空间.
  为了得到最佳性能, 应将maxsize设为2的次方.
  如果传入maxsize=None, 则LRU逻辑将被彻底禁用,
  因此缓存速度更快, 但是条目永远不会被丢弃这可能会消耗过多内存.
  @functools.cache就是如此.
  
typed=False
  决定是否把不同参数类型得到的结果分开保存.
  例如, 在默认设置下, 被认为是值相等的浮点数参数和整数参数只存储一次, 
  即f(1)调用和f(1.0)调用只对应一个缓存条目. 
  如果设为typed=True, 则在不同的条目中存储可能不一样的结果.
  以下示例不使用参数的默认值调用@lru_cache.
@lru_cache(maxsize=2 ** 20, typed=True)
def costly_function(a, b):
    ...
    
接下来介绍另一个强大的装饰器: functools.singledispatch.
9.9.3 单分派泛化函数
假设我们在开发一个调试Web应用程序的工具, 想生成HTML, 以显示不同类型的Python对象.
为此, 可能会编写如下函数.
import html


def htmlize(obj):
	content = html.escape(repr(obj))
	return f'<pre>{content}</pre>'

这个函数适用于任何Python类型, 但是现在我们想扩展一下, 以特别的方式显示如下类型.
str
  把内部的换行符替换为'<br/>\n', 不使用<pre>标签, 而使用<p>.
  
int
  以十进制和十六进制显示数(boo1除外).
  
list
  输出一个HTML列表, 根据各项的类型进行格式化.
  
float和Decimal
  正常输出值, 外加分数形式(为什么不呢?).
  
我们想要的行为如示例9-19所示.
# 示例9-19 生成HTML的htmlize()函数, 调整了几种对象的输出

# htmlize()函数本就针对object, 相当于不匹配其他特殊参数类型时的一种兜底实现.
>>> htmlize({1,2, 3})
'<pre>{1, 2, 3}</pre>'


>>> htmlize(abs)  # &lt; 表示< &gt;表示 >
'<pre>&lt;built-in function abs&gt;</pre>'

# str对象也做HTML转义, 不过是放在<p></P>内, 而且在每个'n'之前插入换行标签<br/>.
>>> htmlize('Heimlich & Co.\n- a game')
'<p>Heimlich &amp; Co.<br/>\n- a game</p>'

# 以十进制和十六进制显示int值, 放在<pre></pre>内.
>>> htmlize(42)
'<pre>42 (@x2a)</pre>'

# 列表中的各项根据类型被格式化, 整个序列会被渲染成一个HTML列表.
>>> print(htmlize(['alpha', 66, [3, 2, 1]])
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>

# bool是int的子类型, 不过得到了特殊对待.
>>> htmlize(True)
'<pre>True</pre>"

# 以分数形式显示Fraction对象
>>> htmlize(fractions.Fraction(2, 3)) 
'<pre>2/3</pre>'

# 以近似的分数显示float值和Decimal值.
>>> htmlize(2/3)
'<pre>0.6666666666666666 (2/3)</pre>'

>>> htmlize(decimal.Decimal('0.02380952'))
'<pre>0.02380952 (1/42)</pre>'

1. 单分派函数
因为Python不支持Java那种方法重载,
所以不能使用不同的签名定义htmlize的变体,以不同的方式处理不同的数据类型. 
在Python中, 常见的做法是把htmlize变成一个分派函数, 
使用一串if/elif/...或match/case/...调用专门的函数, 例如htmlize_str, htmlize_int等.
这样不仅不便于模块的用户扩展, 还显得笨拙: 
时间一长,分派函数htmlize的内容会变得很多,而且它与各个专门函数之间的耦合也太紧密.

functools.singledispatch装饰器可以把整体方案拆分成多个模块,
甚至可以为第三方包中无法编辑的类型提供专门的函数.
使用@singledispatch装饰的普通函数变成了'泛化函数'(generic function, 
指根据第一个参数的类型, 以不同方式执行相同操作的一组函数)的入口. 这才称得上是'单分派'. 
如果根据多个参数选择专门的函数, 那就是'多分派'. 


(单分派(single dispatch)是基于函数的第一个参数的类型来分派不同的函数实现.
在Python中, 可以使用functools库中的singledispatch装饰器来实现单分派.

多分派(multiple dispatch)是基于多个参数的类型来分派不同的函数实现.
在Python中, 可以使用functools库中的singledispatchmethod装饰器
或者第三方库multipledispatch来实现多分派.

简单来说, 单分派和多分派都是基于参数类型来进行分派的泛型函数实现方式, 
不同的是单分派只考虑函数的第一个参数类型, 而多分派可以考虑多个参数类型,)
具体做法如示例9-20所示(单分派).
# 示例9-20 使用@singledispatch创建@htmlize.register 装饰器,
# 把多个函数绑在一起组成一个泛化函数

from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers


# @singledispatch标记的是处理object类型的基函数.
@singledispatch
def htmlize(obj: object) -> str:
	content = html.escape(repr(obj))
	return f'<pre>{content}</pre>'


# 各个专门函数使用@<<basen>>.register装饰.
@htmlize.register  # 运行时传入的第一个参数的类型决定何时使用这个函数.
def _(text: str) -> str:  # 专门函数的名称无关紧要, -是一个不错的选择, 简单明了. ⑤
	content = html.escape(text).replace('\n', '<br/>\n')  
	return f'<p>{content}</p>'  


# 为每个需要特殊处理的类型注册一个函数, 把第一个参数的类型提示设为相应的类型.
@htmlize.register
def _(seq: abc.Sequence) -> str:
	inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
	return '<ul>\n<li>' + inner + '</li>\n</ul>'


# singledispatch支持使用numbers包中的抽象基类. ⑥
@htmlize.register
def _(n: numbers.Integral) -> str:
	return f'<pre>{n}(0x{n:x})</pre>'


# bool 是numbers.Integral的子类型,
# 但是singledispatch逻辑会寻找与指定类型最匹配的实现, 与实现在代码中出现的顺序无关.
@htmlize.register
def _(n: bool) -> str:
	return f'<pre>{n}</pre>!'

# 如果不想或者不能为被装饰的类型添加类型提示, 
# 则可以把类型传给@wbasen.register装饰器. Python3.4或以上版本支持这种句法.
@htmlize.register(fractions.Fraction)
def _(x) -> str:
	frac = fractions.Fraction(x)
	return f'<pre>{frac.numerator}/{frac.denominator}</pre>'


# @<<basen>>.register 装饰器会返回装饰之前的函数.
# 因此可以叠放多个register装饰器, 让同一个实现支持两个或更多类型. ⑦
@htmlize.register(decimal.Decimal)
@htmlize.register(float)
def _(x) -> str:
	frac = fractions.Fraction(x).limit_denominator()
	return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'

# dispatch 多分派
from multipledispatch import dispatch


@dispatch(int, int)
def add(x, y):
	return x + y


@dispatch(int, float)  # 与 (float, int) 不一样的.
def add(x, y):
	return x + y


@dispatch(float, int)
def add(x, y):
	return x + y


@dispatch(float, float)
def add(x, y):
	return x + y


print(add(1, 2))  # Output: 3
print(add(1, 2.0))  # Output: 3.0
print(add(1.0, 2))  # Output: 3.0
print(add(1.0, 2.0))  # Output: 3.0


# singledispatchmethod 多分派
from functools import singledispatchmethod


class MathOperations:
    @singledispatchmethod
    def add(self, x, y):
        raise NotImplementedError("Unsupported types")

    @add.register
    def _(self, x: int, y: int):
        return self._add_integers(x, y)

    @add.register
    def _(self, x: float, y: float):
        return self._add_floats(x, y)

    def _add_integers(self, x, y):
        return x + y

    def _add_floats(self, x, y):
        return x + y


# 创建 MathOperations 的实例
math = MathOperations()

# 调用 add 函数
print(math.add(2, 3))  # 输出: 5
print(math.add(2.5, 3.5))  # 输出: 6.0
print(math.add("Hello, ", "World!"))  # 输出: Unsupported types

5: 可惜, Mypy0.770发现多个同名函数会报错.

6: 尽管8.5.7节中的'论数字塔的倒下'否定了number包中的抽象基类, 
     但是这些抽象基类并没有被弃用, 仍在Python3代码中使用.
     
7: 说不定以后只需要一个未参数化的@htmlize.register, 而在类型提示中使用Union类型.
     我试过, Python会抛出TypeError, 提醒Union不是类.
     可见, 尽管@singledispatch支持PEP484中的句法, 但是语义还未实现.
     
***-----------------------------------------------------------------------------------------***
functools.singledispatch自 Python3.4起就存在了, 不过从Python3.7开始才支持类型提示.
示例9-20中最后两个函数使用的句法支持自Python 3.4起的所有版本.
***-----------------------------------------------------------------------------------------***
应尽量注册处理抽象基类(例如numbers.Integral和abc.MutableSequence)的专门函数,
而不直接处理具体实现(例如int和list).
这样的话, 代码支持的兼容类型更广泛.
例如, Python扩展可以子类化numbers.Integral, 使用固定的位数实现int类型. 

8: 例如, NumPy实现了几个针对机器的整数类型和浮点数类型.
*---------------------------------------------------------------------------------------------*
在单分派中使用抽象基类或typing.Protocol
可以让代码支持抽象基类或实现协议的类当前和未来的具体子类或虚拟子类.
抽象基类的作用和虚拟子类的概念将在第13章讨论.
*---------------------------------------------------------------------------------------------*

singledispatch机制的一个显著特征是, 
你可以在系统的任何地方和任何模块中注册专门函数.
如果后来在新模块中定义了新类型, 则可以轻易添加一个新的自定义函数来处理新类型.
此外, 还可以为不是自己编写的或者不能修改的类编写自定义函数.
(它也支持为不可修改或不可继承的类型编写自定义函数, 进一步增强了函数的可扩展性. )

singledispatch是经过深思熟虑之后才添加到标准库中的, 功能很多, 这里无法一一说明.
'PEP 443-Single-dispatch generic functions'是不错的参考资料,
不过没有讲到类型提示, 毕竟类型提示出现得较晚.
functools模块文档有所改善, singledispatch条目下增加了几个使用类型提示的示例.

**-------------------------------------------------------------------------------------------**
@singledispatch不是为了把Java那种方法重载带入Python.
在一个类中为同一个方法定义多个重载变体比在一个函数中使用一长串if/elif/elif/elif块要好.
但是, 这两种方案都有缺陷, 因为它们让一个代码单元(类或函数)承担的职责太多.
@singledispath的优点是支持模块化扩展:
模块可以为它支持的各个类型注册一个专门的函数.
实践中, 不可能像示例9-20那样把泛化函数的实现都放在同一个模块中.
**-------------------------------------------------------------------------------------------**
目前, 我们见到了几个接受参数的装饰器, 
例如@lru_cache()和示例9-20中使用@singledispatch创建的htmlize.register(float).
9.10节将说明如何构建接受参数的装饰器.
9.10 参数化装饰器
解析源码中的装饰器时, Python会把被装饰的函数作为第一个参数传给装饰器函数.
那么, 如何让装饰器接受其他参数呢, 答案是创建一个装饰器工厂函数来接收那些参数,
然后再返回一个装饰器应用到被装饰的函数上.
是不是有点儿迷惑?
肯定的, 下面以我们目前见到的最简单的装饰器register为例说明,如示例9-21所示.
# 示例9-21中registration.py 模块的删减版, 再次给出, 方便查看
registry = []

def register(func):
    print(f'running register({func})')
    registry.append(func)
    return func

@register
def f1():
    print('running f1()')
    
print('running main()')
print('registry ->',registry)
    
f1()

9.10.1 一个参数化注册装饰器
为了便于启用或禁用register执行的函数注册功能,
为它提供一个可选的active参数, 当设为False时, 不注册被装饰的函数.
实现方式如示例9-22所示.
从概念上看, 这个新的register函数不是装饰器, 而是装饰器工厂函数.
调用register函数才能返回应用到目标函数上的装饰器.
# 示例9-22 为了接受参数, 新的register 装饰器必须作为函数调用

# registry现在是一个set对象, 这样添加和删除函数的速度更快.
registry = set()


# register接受一个可选的关键字参数.
def register(active=True):
	# 内部函数decorate 是真正的装饰器. 注意, 它的参数是一个函数.
	def decorate(func):
		print(f'running register(active={active})->decorate({func})')
		# 只有active参数的值(从闭包中获取)是True时才注册func.
		if active:
			registry.add(func)
		else:
			# 如果active不为True, 而且func在registry中, 那就把它删除.
			registry.discard(func)
		# 因为decorate是装饰器, 所以必须返回一个函数.
		return func

	# register是装饰器工厂函数, 因此返回decorate.
	return decorate


# @register工厂函数必须作为函数调用, 并且传入所需的参数.
@register(active=False)
def f1():
	print('running f1()')


# 即使不传入参数, register也必须作为函数调用(@register()), 返回真正的装饰器decorate.
@register()
def f2():
	print('running f2()')


def f3():
	print('running f3()')

2023-06-08_00001

关键是, register()要返回decorate. 应用到被装饰的函数上的是decorate.
示例9-22中的代码在registration_param.py 模块中.
导入该模块, 得到的结果如下所示.
>>> import registration_param
running register(active=False)->decorate(<function f1 at 0x10063c1e0>)
running register(active=True)->decorate(<function f2 at 0x10063c268>)

>>> registration_param.registry
[<function f2 at 0x10063c268>]
注意, 只有f2出现在了registry中, f1不在其中, 因为传给register装饰器工厂函数的参数是active=False,
所以应用到f1上的decorate 没有把它添加到registry中.

如果不使用@句法, 那么就要像常规函数那样调用register.
如果想把f添加到registry中, 那么装饰f函数的句法是register()(f);
如果不想添加f(或把它删除), 则句法是register(active=False)(f).
示例9-23演示了如何把函数添加到registry中, 以及如何从中删除函数.
# 示例9-23 使用示例9-22中的registration_param模块
>>> from registration_param import *
running register(active=False)->decorate(<function f1 at Ox10073c1e0>)  # register加()执行的结果
running register(active=True)->decorate(<function f2 at 0x10073c268>)  # register加()执行的结果

# 导入这个模块时, f2在registry中.
>>> registry
[<function f2 at 0x10073c268>]

# register()表达式返回decorate并应用到f3上.
>>> register()(f3)
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>  # 返回被装饰的函数对象.


# 前一行把f3添加到registry中. (现在就两个了.)
>>> registry 
[<function f3 at 0x10073c158>, <function f2 at Ox10073c268>]

# 这个调用从registry中删除f2.
>>> register(active=False)(f2)
running register(active=False)->decorate(<function f2 at Ox10073c268>)
<function f2 at 0x10073c268>  # 返回被装饰的函数对象.

# 确认registry中只有f3.
>>registry 
[<function f3 at 0x10073c158>]

参数化装饰器的原理相当复杂, 刚刚讨论的那个例子比大多数例子简单.
参数化装饰器通常会把被装饰的函数替换掉, 而且结构上需要多一层嵌套.
接下来会探讨这种函数金字塔.
9.10.2 参数化clock装饰器
本节再次探讨clock装饰器, 为它添加一个功能: 
让用户传入一个格式字符串, 控制被装饰函数的输出, 如示例9-24所示.
# 示例9-24 clockdeco_param.py 模块: 数化clock装饰器
import time

# 显示函数运行时间, 函数名, 函数的位置参数, 返回值.
DEFAULT_FMT = '[{elapsed: 0.8f}s] {name}({args}) -> {result}'


# clock 是参数化装饰器工厂函数.
def clock(fmt=DEFAULT_FMT):
    # decorate 是真正的装饰器.
    def decorate(func):
        # clocked 包装被装饰的函数.
        def clocked(*_args):
            to = time.perf_counter()
            # _result 是被装饰的函数返回的真正结果.
            _result = func(*_args)
            # 计算运行时间.
            elapsed = time.perf_counter() - to
            # 获取函数名称.
            name = func.__name__
            # _args用于存放clocked的真正参数, args是用于显示的字符串。
            args = ', '.join(repr(arg) for arg in _args)
            # result 是_result的字符串表示形式, 用于显示.
            result = repr(_result)
            # 这里使用**locals()是为了在fmt中引用clocked的局部变量. ⑨
            print(fmt.format(**locals()))
            # clocked 将取代被装饰的函数, 因此它应该返回被装饰的函数返回的值。
            return _result
        # decorate 返回 clocked.
        return clocked
	# clock返回decorate.
    return decorate


@clock()
def snooze(seconds):
    time.sleep(seconds)


for i in range(3):
    snooze(.123)

9: 技术审校Miroslav Sedivy指出: 
但是, 代码lint程序将报错, 提醒有未使用的变量, 因为lint程序往往忽略locals().'
是的, 这再一次表明静态检查工具不鼓励使用Python的动态功能,
而我和无数的程序员最初就是被这些动态功能吸引才爱上Python的.
为了让lint程序满意, 每个局部变量都要写两次: 
fnt.format(elapsed=elapsed, name=name, args=args, result=result).
我可不想这么麻烦. 使用静态检查工具时, 一定要知道何时应忽略工具报告的错误.
在当前模块中测试, 调用clock()时不传入参数, 因此所应用的装饰器将使用默认的格式字符串为简单起见,
示例9-24基于示例9-14中最初实现的clock, 而不是示例9-16中使用@functools.wraps改进后的版本, 
因为那一版增加了一层函数.
在shell中运行示例9-24, 得到的结果如下所示.
$ python3 clockdeco_param. py
[0.12412500s] snooze(0.123) -> None 
[0.12411904s] snooze(0.123) -> None
[0.12410498s] snooze(0.123) -> None

示例9-25和示例9-26中的两个模块也使用clockdeco_param, 两个示例的下方给出了输出结果.
# 示例9-25 clockdeco param demol.py
import time
from clockdeco_param import clock


@clock('{name}: {elapsed}s')
def snooze(seconds):
    time.sleep(seconds)
    
    
for i in range(3):
    snooze(.123)
    
以上示例的输出如下所示.
$ python3 clockdeco_param_demo1.py
snooze: 0.12414693832397461s
snooze: 0.1241159439086914s
snooze: 0.12412118911743164s
# 示例9-26 clockdeco _param_demo2.py
import time
from clockdeco_param import clock


@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
    time.sleep(seconds)
    
    
for i in range(3):
snooze(.123)

以上示例的输出如下所示.
$ python3 clockdeco_param_demo2.py
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
**-------------------------------------------------------------------------------------------**
本书第1版技术审校之一LennartRegebro认为, 
装饰器最好通过定义了__call__方法的类实现, 不应像本章这样通过函数实现.
我同意类更适合创建重要的装饰器, 但是为了讲解这个语言功能的基本思想, 函数更易于理解.
9.12节提到了一些工业级装饰器构建技术, 尤其不要错过Graham Dumpleton的博客文章和wrapt模块.
**-------------------------------------------------------------------------------------------**
9.10.3节会举一个例子, 按照Regebro和Dumpleton建议的方式创建装饰器.
9.10.3 基于类的clock装饰器
再举一个例子.
示例9-27通过定义__call__方法的类实现了参数化装饰器clock.
请对比一下示例9-24和示例9-27, 你更喜欢哪一个?
# 示例9-27 clockdeco_cls.py 模块: 通过类实现参数化装饰器clock
import time

DEFAULT_FMT = '[{elapsed: 0.8f}s] {name}({args}) -> {result}'


# 不用定义外层函数clock了, 现在clock类是参数化装饰器工厂.
# 类名使用的是小写字母c, 以此表明这里的实现可以直接替代示例9-24.
class clock:
    # clock(my_format)传入的参数赋值给这里的fmt参数.
    # 类构造函数返回一个clock实例, my_format被存储为self.fmt.
    def __init__(self, fmt=DEFAULT_FMT):
        self.fmt = fmt
	
    # 有了__call__方法, clock实例就成为可调用对象了.
    # 调用实例的结果是把被装饰的函数替换成clocked.
    def __call__(self, func):
        def clocked(*_args):
            to = time.perf_counter()
            # clocked 包装被装饰的函数.
            _result = func(*_args)
            elapsed = time.perf_counter() - to
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)
            result = repr(_result)
            print(self.fmt.format(**locals()))
            return _result

        return clocked

    
def snooze(seconds):
    time.sleep(seconds)

# 实例化.
clock_obj = clock()
# 传入被装饰的函数.
snooze = clock_obj(snooze)

for i in range(3):
    snooze(.123)

对函数装饰器的讨论到此结束. 24章会介绍类装饰器.
9.11 本章小结
本章涉及一些难以理解的内容.
学习之路崎岖不平, 我已经尽可能让路途平坦顺畅.
毕竟, 我们已经进入元编程领域了. (你了不起, 不清高...)

本章以编写一个没有内部函数的@register装饰器作为起始, 最后实现了有两层嵌套函数的参数化装饰器@clock().

尽管注册装饰器十分简单, 但其是在Python框架中有用武之地.
10章将使用这种注册方式实现一个'策略'设计模式.

如果想真正理解装饰器, 则不仅需要区分导入时和运行时, 还要理解变量作用域, 闭包和新增的nonlocal声明.
掌握闭包和nonlocal不仅对构建装饰器有帮助, 在面向事件的GUI程序编程和基于回调处理异步IO中也用得到,
遇到适合使用函数式编程的情况更能得心应手.

参数化装饰器基本上涉及至少两层嵌套函数, 如果想使用@functools.wraps生成装饰器, 
为高级技术提供更好的支持, 则嵌套层级可能会更深, 例如示例9-18中叠放的装饰器.
对更复杂的装饰器来说, 基于类实现或许更易于理解和维护.

本章还以functools模块中强大的@cache和@singledispatch为例, 介绍了标准库中的参数化装饰器.
9.12 延伸阅读
<<Effective Python: 编写高质量Python代码的90个有效方法(原书第2)>>
一书中的第26条实践原则给出了函数装饰器的最佳实践, 建议始终使用functools.wraps(像示例9-16那样). ?
(10: 为了尽量保证代码简单易懂, 本书中并不是所有示例都遵从Brett Slatkin(<<Effective Python:
编写高质量Python代码的90个有效方法(原书第2)>>的作者)的优秀建议.)

Graham Dumpleton写了一系列博客文章, 深入剖析了如何实现行为良好的装饰器,
第一篇是'How you implemented your Python decorator is wrong'.
他在这方面的渊博知识充分体现在他编写的wrapt模块中.
这个模块旨在简化装饰器和动态函数包装器的实现, 即使多层装饰也支持内省, 而且行为正确, 
既可以应用到方法上, 也可以作为属性描述符使用. 
23章会讨论描述符.

<<Python Cookbook (3) 中文版>> 的第9章中的几个经典实例构建了基本的装饰器和特别复杂的装饰器,
其中, 9.6节提到的装饰器既可以作为常规的装饰器调用(例如@clock),
也可以作为装饰器工厂函数调用(例如@clock() ).

Michele Simionato开发了一个包, 根据文档, 
该包旨在简化普通程序员使用装饰器的方式, 并且通过各种复杂的示例推广装饰器'.
这个包是decorator, 可以通过PyPI安装.

Python Decorator Library 维基页面在Python刚添加装饰器功能时就创建了, 里面有十几个示例.
那个页面是几年前开始编写的, 虽然有些技术已经过时了, 但仍是很棒的灵感来源.

Fredrik Lundh写的一篇简短的博客文章'Closures in Python'解读了闭包这个术语.

'PEP 3104--Access to Names in Outer Scopes'说明了引入nonlocal声明的原因:
重新绑定既不在局部作用域中也不在全局作用域中的名称.
这份PEP还概述了其他动态语言(Perl, Ruby, JavaScript等)解决这个问题的方式,
以及在Python中几种可用的设计方案的优缺点.

'PEP 227—Statically Nested Scopes'更偏重理论, 说明了Python 2.1引入的词法作用域.
词法作用域在Python 2.1中是一种备选方案, 到Python2.2变成了标准方案.
此外, 这份PEP还说明了Python中闭包的基本原理和实现方式的选择.

PEP 443对单分派泛化函数的基本原理和细节做了说明.
Guido van Rossum很久以前(20053)写的一篇博客文章'Five--Minute Multimethods in Python'
详细说明了如何使用装饰器实现泛化函数(也叫多方法, multimethod).
他给出的代码支持多分派(根据多个位置参数进行分派).
虽然Guido写的多方法代码很棒, 但那只是教学示例.
如果想使用现代的技术实现多分派泛化函数并在生产环境中使用, 可以用Martijn Faassen开发的Reg.
Martijn还是模型驱动型REST式Web框架Morepath的开发者.
**---------------------------------------------杂谈-------------------------------------------**
'动态作用域与词法作用域':
任何把函数当作一等对象的语言其设计者都面对一种情况:
作为一等对象, 在某个作用域中定义的函数, 可能会在其他作用域中调用.
问题是, 如何求解自由变量?
首先出现的最简单的处理方式是使用'动态作用域'. 也就是说, 根据调用函数的环境求解自由变量.
如果Python使用动态作用域, 不支持闭包, 那么就可以像下面这样使用avg函数(与示例9-8类似).
>>> ### 这不是真实的Python控制台会话! ###
>>> avg = make_averager()

# 使用avg之前要自己定义series = [], 
# 因此必须知道averager(在make_averager内部)引用的是名为series的列表.
>>> series = []
>>> avg(10)
10.0
# 在背后使用series累计要计入平均值的值.
>>> avg(11)
10.5
>>>avg(12)
11.0
# 执行series=[1]后, 之前的列表消失了. 同时计算两个独立的累计平均值时, 可能发生这种意外.
>>> series = [1]
>>> avg(5)
3.0

# Python支持闭包, 可以把series的实现隐藏起来, 不然用户直接操作.

函数应该是黑盒, 把实现隐藏起来, 不让用户知道.
但是对动态作用域来说, 如果函数使用自由变量, 那么程序员就必须知道函数的内部细节,
这样才能建立正确运行所需的环境.
多年前, 我与文档准备语言LaTeX结下不解之缘, 
直到读了Practical LaTeX(George Gr?tzer著)一书才意识到, LaTeX变量使用的是动态作用域.
难怪我以前觉得LaTeX变量那么今人费解!

Emacs Lisp也使用动态作用域, 至少默认如此.
Emacs Lisp手册中的'Dynamic Binding'一节对此有简短说明.

动态作用域易于实现, 
这大概就是John McCarthy 创建Lisp(第一门把函数视作一等对象的语言)时采用这种方式的原因.
Paul Graham 写的'The Roots of Lisp'一文对John McCarthy关于Lisp语言的题为
'Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I'
的论文做了通俗易懂的解读, 这篇论文是和贝多芬第九交响曲一样伟大的杰作.
Paul Graham使用通俗易懂的语言翻译了这篇论文, 把数学原理转换成了英语和可运行的代码.

Paul Graham的解读还指出, 动态作用域难以实现.
下面这段文字引自'The Roots of Lisp'一文.
  就连第一个Lisp高阶函数示例都因为动态作用域而无法运行, 这充分证明了动态作用域的危险性.
  McCarthy在1960年可能没有全面认识到动态作用域的影响.
  动态作用域在各种Lisp实现中存在的时间特别长, 直到Sussman和Steele在1975年开发出Scheme为止.
  词法作用域不会导致eval的定义变得多么复杂, 只是编译器可能更难编写.

如今, 词法作用域已成常态: 根据定义函数的环境求解自由变量. (Python是如此, 在函数定义阶段确定变量关系.)
词法作用域让人更难实现支持一等函数的语言, 因为需要支持闭包.
不过, 词法作用域让代码更易于阅读. Algol之后出现的语言大多使用词法作用域.
值得注意的是, JavaScript是个例外.
在JavaScript中, 特殊变量this最让人摸不着头脑, 因为根据代码的编写方式, 
它既可以使用词法作用域, 也可以使用动态作用域.

多年来, 由于Python的lambda表达式不支持闭包, 因此在博客圈的函数式编程极客群体中, 这个功能的名声并不好.
Python2.2(200112月发布)修正了这个问题, 但是博客圈的固有印象不会轻易转变.
自此之后, 仅仅由于句法上的局限, lambda一直处于尴尬的境地.
'Python 装饰器和装饰器设计模式':
Python函数装饰器符合<<设计模式>>一书中对装饰器模式的一般描述:
'动态地给一个对象添加一些额外的职责. 就扩展功能而言, 装饰器模式比子类化更灵活.'

在实现层面, Python装饰器与装饰器设计模式不同, 但是有些相似之处.

在设计模式中, Decorator和Component是抽象类.
为了给具体组件添加行为, 具体装饰器的实例要包装具体组件的实例/
<<设计模式>>是这样说的:
  装饰器与其所装饰的组件接口一致, 因此它对使用该组件的客户透明.
  它会将客户请求转发给该组件, 并且可能在转发前后执行一些额外的操作(例如绘制一个边框).
  透明性使得你可以递归嵌套多个装饰器, 从而可以添加任意多的功能.
  
在Python中, 装饰器函数相当于Decorator的具体子类, 而装饰器返回的内部函数相当于装饰器实例.
返回的函数包装了被装饰的函数, 这相当于设计模式中的组件.
返回的函数是透明的, 因为它接受相同的参数, 符合组件的接口.
返回的函数会把调用转发给组件, 并可以在转发前后执行额外的操作.
因此, 前面引用那段话的最后一句可以改成: '透明性使得你可以叠放多个装饰器, 从而可以添加任意多的行为.'
注意, 我并不是建议在Python程序中使用函数装饰器实现装饰器模式.
在特定情况下确实可以这么做, 但是一般来说, 实现装饰器模式时最好使用类表示装饰器和要包装的组件.

**-------------------------------------------------------------------------------------------**
文章来源:https://blog.csdn.net/qq_46137324/article/details/135725587
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。