流畅的python--第九章 装饰器和闭包
装饰器基础知识
装饰器是一种可调用对象,其参数是另一个函数(被装饰的函数)。装饰器可能会对被装饰的函数做些处理,然后返回函数,或者把函数替换成另一个函数或可调用对象。
假如有一个名为decorate的装饰器:
@decorate
def target():
print("running target()")
以下写法与上面的效果一样。
def target():
print('running target()')
target = decorate(target)
两种写法的最终结果一样:上面两个代码片段执行完毕后,target名称都会绑定decorate(target)返回的函数--可能是原来那个名为target的函数,也可能是另一个函数。
示例9-1 装饰器通常会把一个函数替换成另一个函数

严格来说,装饰器只是语法糖。如前所述,装饰器可以像常规的可调用对象那样调用,传入另一个函数。有时,这样做其实更方便,尤其是做元编程(在运行时改变程序的行为)时。
综上所述,装饰器有以下3个基本性质:
- 装饰器是一个函数或其他可调用对象
- 装饰器可以把被装饰的函数替换成别的函数
- 装饰器在加载模块时立即执行
Python何时执行装饰器
装饰器的一个关键性质是,它们在被装饰的函数定义之后立即执行。这通常是在导入时(例如,当python加载模块时)。
示例9-2 registration.py 模块

把registration.py当做脚本运行,得到的输出如下所示。

🚩
register在模块中其他函数之前运行(该例中是两次)。调用register时,传给它的参数是被装饰的函数,例如<function f1 at 0x100631bf8>。
通过debug,在第一行 registry = []前面设置断点进行debug,代码执行顺序如下:

如果在main()处设置断点进行debug,代码执行顺序如下:

但是结果显示

则说明装饰器在main函数执行之前就已经执行了,因此多了红框的两个结果。
加载模块后,registry中有两个被装饰函数(f1和f2)的引用。这两个函数以及f3,只在main显示调用它们时才执行。如果是导入registration.py模块(不作为脚本运行),则输出如下所示。

这种情况下,如果查看registry的值,则得到的输出如下所示。

示例9-2主要强调,函数封装器在导入模块时立即执行,而被装饰的函数只在显示调用时运行。因此导入时和运行时存在区别。
注册装饰器
考虑到装饰器在真实代码中的常用方式,示例9-2有两处不寻常的地方。
- 装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装饰器通常在一个模块中定义,然后再应用到其他模块中的函数上。
- register装饰器返回的函数与通过参数传入的函数相同。实际上,大多数装饰器会在内部定义一个函数,然后将其返回。
大多数装饰器会更改被装饰的函数。通常的做法是,返回在装饰器内部定义的函数,取代被装饰的函数。涉及内部函数的代码基本上离不开闭包。为了理解闭包,需要后退一步,先研究Python中的变量作用域规则。
变量作用域规则
示例9-3 一个函数,该函数会读取一个局部变量和一个全局变量

示例9-3定义并测试了一个函数,该函数会读取两个变量的值:一个是通过函数的参数传入的局部变量a,另一个是函数没有定义的变量b。
出现错误并不奇怪。接着示例9-3,如果先给全局变量b赋值,然后再调用f1,则不会出现错误。

示例9-4 b是局部变量,因为在函数主体中给它赋值了

注意,首先输出的是3,这表明执行了print(a)语句。但是,第二个语句,即print(b),绝对不会执行。事实上,Python编译函数主体时,判断b是局部变量,因为在函数内给它赋值了。生成的字节码正是了这种判断。所以,
Python会尝试从局部作用域中获取b。后面调用f2(3)时,f2的主体顺利获取并打印局部变量a的值,但是在尝试获取局部变量b的值时,发现b没有绑定值。
🚩
Python不要求声明变量,但是会假定在函数主体中赋值的变量是局部变量。这比JavaScript的行为好多了,JavaScript也不要求声明变量,但是如果忘记把变量声明为局部变量(使用var),则可能会在不知情的情况下破坏全局变量。
在函数中赋值时,如果想让解释器把b当成全局变量,为它分配一个新值,就要使用global声明。

可以发现有两种作用域:
- 模块全局作用域
在类或函数块外部分配值的名称 - f3函数局部作用域
通过参数或者在函数主体中直接分配值的名称
变量还有可能出现在第 3 个作用域中,我们称之为“非局部”作用域。这
个作用域是闭包的基础,稍后详述。
比较字节码
dis模块为反汇编Python函数字节码提供了简单的方式。示例9-5和示例9-6分别是示例9-3中的f1和示例9-4中的f2的字节码。
示例9-5 反汇编示例9-3中的f1函数

几点说明:
0 (print)加载全局名称print0 (a)加载局部名称a1 (b)加载全局名称b
示例9-6 反汇编示例9-4中的f2函数
![]()
1 (b)加载局部名称b。这表明,虽然在后面才为b赋值,但是编译器
会把b视作局部变量,因为变量的种类(是不是局部变量)在函数
主体中不能改变。
🚩运行字节码的
CPython虚拟机(virtual machine,VM)是栈机器,
因此LOAD操作和POP操作引用的是栈。
闭包
闭包是延伸了作用域的函数,包括函数(f)主体中引用的非全局变量和局部变量。这些变量必须来自包含f的外部函数的局部作用域。
函数是不是匿名的没有关系,关键是它能访问主体之外定义的非全局变量。
假如有个名为avg的函数,它的作用是计算不断增加的系列值的平均值,例如计算整个历史中某个商品的平均收盘价。新价格每天都在增加,因此计算平均值时要考虑到目前为止的所有价格。
示例9-7 average_oo.py: 一个计算累计平均值的类

示例9-8 average.py: 一个计算累计平均值的高阶函数

调用make_averager,返回一个averager函数对象,每次调用,averager都会把参数添加到系列值中,然后计算当前平均值。
注意这两个示例有相似之处:调用Averager()或make_averager()得到一个可调用对象avg,它会更新历史值,然后计算当前平均值。在示例9-7中avg是Averager类的实例,在示例9-8中,avg是内部函数averager。不管怎样,只需要调用
avg(n),把n放入系列值中,然后重新计算平均值即可。
作为 Averager 类的实例,avg 在哪里存储历史值很明显:实例属性self.series。
注意,series是make_averager函数的局部变量,因为赋值语句series = []在make_averager函数的主体中。但是调用avg(10)时,make_averager函数已经返回,局部作用域早就消失。

图9-1: averager函数的闭包延伸到自身的作用域之外,包含自由变量series的绑定
如图所示,在averager函数中,series是自由变量(free variable)。自由变量是一个术语,指未在局部作用域中绑定的变量。
示例9-10 查看make_averager创建的函数

series的值在返回的avg函数的__closure__属性中。avg.__closure__中的各项对应avg.__code__.co_freevars
中的一个名词。这些项是cell对象,有一个名为cell_contents的属性,保存着真正的值。
示例9-11 接续示例9-9

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

问题是,对于数值或任何不可变类型,count += 1 语句的作用其实与
count = count + 1 一样。因此,实际上我们在 averager 的主体中
为 count 赋值了,这会把 count 变成局部变量。total 变量也受这个
问题影响。示例 9-8 则没有这个问题,因为没有给 series 赋值,只是调用
series.append,并把它传给 sum 和 len。也就是说,我们利用了“列
表是可变对象”这一事实。但是,数值、字符串、元组等不可变类型只能读取,不能更新。如果像
count = count + 1 这样尝试重新绑定,则会隐式创建局部变量。为了解决上面的问题,引入了nonlocal关键字。它的作用是把
变量标记为自由变量,即便在函数中为变量赋予了新值。如果为
nonlocal 声明的变量赋予新值,那么闭包中保存的绑定也会随之更新。
count。如此一来,count 就不是自由变量了,因此不会保存到闭包中。
示例9-13 计算累计平均值,不保存所有历史(使用nonlocal修正)

变量查找逻辑
Python 字节码编译器根据以下规则获取函数主体中出现的变量 x。
- 如果是
global x声明,则x来自模块全局作用域,并赋予那个作用域中x的值。 - 如果是
nonlocal x声明,则 x 来自最近一个定义它的外层函数,并赋予那个函数中局部变量x的值。 - 如果
x是参数,或者在函数主体中赋了值,那么x就是局部变量。 - 如果引用了
x,但是没有赋值也不是参数,则遵循以下规则。- 在外层函数主体的局部作用域(非局部作用域)内查找
x。 - 如果在外层作用域内未找到,则从模块全局作用域内读取。
- 如果在模块全局作用域内未找到,则从
__builtins__.__dict__中读取。
- 在外层函数主体的局部作用域(非局部作用域)内查找
实现一个简单的装饰器
示例9-14 clockdeco0.py:一个会显示函数运行时间的简单的装饰器
import time
def clock(func):
def clocked(*args):# 定义内部函数 clocked,它接受任意个位置参数
t0 = time.perf_counter()
result = func(*args)
# 这行代码行之有效,因为 clocked 的闭包中包含自由变量 func。
elasped = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print(f'[{elasped:0.8f}s] {name}({arg_str}) - > {result!r}')
return result
return clocked # 返回内部函数,取代被装饰的函数。
示例9-15 使用clock装饰器, clock_test.py
import time
from clockdeco0 import clock
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
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)

工作原理:
如前所述,以下代码:
@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__ 属
性,会看到如下结果。

可见,现在 factorial 保存的其实是 clocked 函数的引用。自此之
后,每次调用 factorial(n) 执行的都是 clocked(n)。clocked 大致
做了下面几件事。
- 记录初始时间
t0。 - 调用原来的
factorial函数,保存结果。 - 计算运行时间。
- 格式化收集的数据,然后打印出来。
- 返回第 2 步保存的结果。
这是装饰器的典型行为:把被装饰的函数替换成新函数,新函数接受的
参数与被装饰的函数一样,而且(通常)会返回被装饰的函数本该返回
的值,同时还会做一些额外操作。
示例 9-14 实现的clock装饰器有几个缺点:不支持关键字参数,而且
遮盖了被装饰函数的__name__属性和__doc__属性。示例 9-16 使用
functools.wraps装饰器把相关的属性从func身上复制到了
clocked中。此外,这个新版还能正确处理关键字参数。
示例9-16clockdeco.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。
示例 9-16 用到了另一个重要的装饰器,即 functools.wraps。它的作
用是协助构建行为良好的装饰器。标准库中最吸引人的几个装饰器,即
cache、lru_cache 和 singledispatch,均来自 functools 模块。
使用 functools.cache 做备忘
functools.cache 装饰器实现了备忘(memoization)。 这是一项优
化技术,能把耗时的函数得到的结果保存起来,避免传入相同的参数时
重复计算。
生成第 n 个斐波那契数这种慢速递归函数适合使用 @cache,如示例 9-
17 所示。
示例9-17 生成第n个斐波那契数列,递归方式非常耗时 fibo_demo.py
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 装饰器生成的。

浪费时间的地方很明显:fibonacci(1) 调用了 8 次,fibonacci(2)
调用了 5 次……但是,如果增加两行代码,使用 cache,那么性能将显
著改善。
示例9-18 使用缓存实现,速度更快
import functools
from clockdeco import clock
@functools.cache
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
print(fibonacci(6))

两点注意点:
@functools.cache可在Python 3.9或以上版本中使用@clock这里叠放了装饰器:@cache应用到@clock返回的函数上
叠放装饰器
如果想理解叠放装饰器,那么需要记住一点:@ 是一种语法糖,其
作用是把装饰器函数应用到下方的函数上。多个装饰器的行为就像
调用嵌套函数一样。
@alpha
@beta
def my_fn():
...
等同于以下内容。
my_fn = alpha(beta(my_fn))
也就是说,首先应用 beta 装饰器,然后再把返回的函数传给
alpha。
如果要计算 fibonacci(30),使用示例 9-18 中的版本,总计会调用 31
次,耗时 0.000 17 秒,而示例 9-17 中未做缓存的版本在配有 Intel Core i7 处理器的笔记本计算机中则耗时 12.09 秒,因为仅 fibonacci(1) 就
要调用 832 040 次,总计调用 2 692 537 次。
被装饰的函数所接受的参数必须可哈希,因为底层 lru_cache 使用
dict 存储结果,字典的键取自传入的位置参数和关键字参数。
除了优化递归算法,@cache 在从远程 API 中获取信息的应用程序中也
能发挥巨大作用。
🚩 如果缓存较大,则
functools.cache有可能耗尽所有可用
内存。在我看来,@cache更适合短期运行的命令行脚本使用。对
于长期运行的进程,推荐使用functools.lru_cache,并合理设
置maxsize参数。
使用 lru_cache
functools.cache 装饰器只是对较旧的functools.lru_cache 函数
的简单包装。其实,functools.lru_cache 更灵活,而且兼容 Python3.8 及之前的版本。
@lru_cache 的主要优势是可以通过 maxsize 参数限制内存用量上
限。maxsize参数的默认值相当保守,只有 128,即缓存最多只能有128 条。LRU 是“Least Recently Used”的首字母缩写,表示一段时间不用的缓存
条目会被丢弃,为新条目腾出空间。
从 Python 3.8 开始,lru_cache` 有两种使用方式。下面是最简单的方式。
- 其一:
@lru_cache
def costly_function(a, b):
...
- 其二:
@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):
...
单分派泛化函数
假设我们在开发一个调试 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
以十进制和十六进制显示数(bool除外)。list
输出一个HTML列表,根据各项的类型进行格式化float和Decimal
正常输出值,外加分数形式
示例9-19 生成 HTML 的 htmlize() 函数,调整了几种对象的输出


因为 Python 不支持Java 那种方法重载,所以不能使用不同的签名定义
htmlize 的变体,以不同的方式处理不同的数据类型。在 Python 中,
常见的做法是把 htmlize 变成一个分派函数,使用一串 if/elif/...或 match/case/... 调用专门的函数,例如
htmlize_str、htmlize_int 等。这样不仅不便于模块的用户扩展,
还显得笨拙:时间一长,分派函数 htmlize 的内容会变得很多,而且它与各个专门函数之间的耦合也太紧密。
functools.singledispatch 装饰器可以把整体方案拆分成多个模
块,甚至可以为第三方包中无法编辑的类型提供专门的函数。使用
singledispatch 装饰的普通函数变成了泛化函数(generic function,
指根据第一个参数的类型,以不同方式执行相同操作的一组函数)的入
口。这才称得上是单分派。如果根据多个参数选择专门的函数,那就是
多分派。
示例9-20 使用 @singledispatch 创建 @htmlize.register 装
饰器,把多个函数绑在一起组成一个泛化函数
from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers
@singledispatch #❶
def htmlize(obj: object) -> str:
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'
@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>'
@htmlize.register #❺
def _(n: numbers.Integral) -> str:
return f'<pre>{n} (0x{n:x})</pre>'
@htmlize.register #❻
def _(n: bool) -> str:
return f'<pre>{n}</pre>'
@htmlize.register(fractions.Fraction) #❼
def _(x) -> str:
frac = fractions.Fraction(x)
return f'<pre>{frac.numerator}/{frac.denominator}</pre>'
@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>'
❶ @singledispatch 标记的是处理 object 类型的基函数。
❷ 各个专门函数使用 @«base».register 装饰。
❸ 运行时传入的第一个参数的类型决定何时使用这个函数。专门函数
的名称无关紧要,_ 是一个不错的选择,简单明了
❹ 为每个需要特殊处理的类型注册一个函数,把第一个参数的类型提
示设为相应的类型。
❺ singledispatch 支持使用 numbers 包中的抽象基类。
❻ bool 是 numbers.Integral 的子类型,但是 singledispatch逻辑
会寻找与指定类型最匹配的实现,与实现在代码中出现的顺序无关。
❼ 如果不想或者不能为被装饰的类型添加类型提示,则可以把类型传
给 @«base».register 装饰器。Python 3.4 或以上版本支持这种句法。
❽ @«base».register 装饰器会返回装饰之前的函数,因此可以叠放
多个 register 装饰器,让同一个实现支持两个或更多类型。
应尽量注册处理抽象基类(例如 numbers.Integral 和
abc.MutableSequence)的专门函数,而不直接处理具体实现(例如
int 和 list)。这样的话,代码支持的兼容类型更广泛。例如,Python
扩展可以子类化 numbers.Integral,使用固定的位数实现 int 类型。
在单分派中使用抽象基类或 typing.Protocol 可以让代码支
持抽象基类或实现协议的类当前和未来的具体子类或虚拟子类。
singledispatch 机制的一个显著特征是,你可以在系统的任何地方和
任何模块中注册专门函数。如果后来在新模块中定义了新类型,则可以
轻易添加一个新的自定义函数来处理新类型。此外,还可以为不是自己
编写的或者不能修改的类编写自定义函数。
singledispatch 是经过深思熟虑之后才添加到标准库中的,功能很
多,这里无法一一说明。“PEP 443—Single-dispatch generic functions”是
不错的参考资料,不过没有讲到类型提示,毕竟类型提示出现得较
晚。functools 模块文档有所改善,singledispatch 条目下增加了几
个使用类型提示的示例。
参数化装饰器
解析源码中的装饰器时,Python 会把被装饰的函数作为第一个参数传给
装饰器函数。那么,如何让装饰器接受其他参数呢?答案是创建一个装
饰器工厂函数来接收那些参数,然后再返回一个装饰器,应用到被装饰
的函数上。是不是有点儿迷惑?肯定的。下面以我们目前见到的最简单
的装饰器 register 为例说明,如示例 9-21 所示。
示例9-21 示例 9-2 中 registration.py 模块的删减版,再次给出,方便查看

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

❶ registry 现在是一个 set 对象,这样添加和删除函数的速度更快。
❷ register 接受一个可选的关键字参数。
❸ 内部函数 decorate 是真正的装饰器。注意,它的参数是一个函数。
❹ 只有 active 参数的值(从闭包中获取)是 True 时才注册 func。
❺ 如果 active 不为 True,而且 func 在 registry 中,那就把它删
除。
❻ 因为 decorate 是装饰器,所以必须返回一个函数。
❼ register是装饰器工厂函数,因此返回 decorate。
❽ @register 工厂函数必须作为函数调用,并且传入所需的参数。
❾ 即使不传入参数,register 也必须作为函数调用(@register()),返回真正的装饰器 decorate。
关键是,register() 要返回 decorate。应用到被装饰的函数上的是decorate。
参数化 clock 装饰器
示例 9-24 clockdeco_param.py 模块:参数化 clock 装饰器
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): #❶
def decorate(func): #❷
def clocked(*_args): #❸
t0 = time.perf_counter()
_result = func(*_args) #❹
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) #❺
result = repr(_result)#❻
print(fmt.format(**locals())) #❼
return _result #❽
return clocked #❾
return decorate #❿
if __name__ == '__main__':
@clock() #⓫
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
❶ clock 是参数化装饰器工厂函数。
❷ decorate 是真正的装饰器。
❸ clocked 包装被装饰的函数。
❹ _result 是被装饰的函数返回的真正结果。
❺ _args 用于存放 clocked 的真正参数,args 是用于显示的字符串。
❻ result 是 _result 的字符串表示形式,用于显示。
❼ 这里使用 **locals() 是为了在 fmt 中引用 clocked 的局部变量
❽ clocked 将取代被装饰的函数,因此它应该返回被装饰的函数返回
的值。
❾ decorate 返回 clocked。
❿ clock 返回 decorate。
⓫ 在当前模块中测试,调用 clock() 时不传入参数,因此所应用的装
饰器将使用默认的格式字符串。

示例9-25 clockdeco_param_demo1.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)

示例 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)

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


浙公网安备 33010602011771号