流畅的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)
加载全局名称print
0 (a)
加载局部名称a
1 (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
包装被装饰的函数。