CHAR.VI 函数装饰器和闭包
函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。
nonlocal 是新近出现的保留关键字,在Python3中引入。作为Python程序员,如果严格遵守基于类的面向对象编程方式,即便不知道这个关键字也不会受到影响。然而如果想自己实现函数装饰器,那就必须了解闭包的方方面面,因此也就需要知道nonlocal。
除了在装饰器中有用处之外,闭包还是回调式异步编程 和 函数式编程风格 的基础。
本章最终目标是解释清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的参数化装饰器。但是在讨论这些之前要讨论以下话题:
- Python如何计算装饰器句法
- Python如何判断变量是不是局部的
- 闭包存在的原因是工作原理
- nonlocal 能解决什么问题
掌握这些基础知识后,我们可以进一步讨论装饰器:
- 实现行为良好的装饰器
- 标准库中有用的装饰器
- 实现一个参数化标准库
下面首先介绍装饰器的基础知识,然后再讨论上面的各个话题。
7.1 装饰器基础知识
装饰器是可调用的对象,其参数是另一个参数(被修饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
假如有个名叫decorate的装饰器:
@decorate
def target():
print('running target()')
上述代码的效果与下述写法一样:
def target():
print('running target()')
target = decorate(target)
严格地说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做 元编程(在运行时改变程序的行为)时。
综上,装饰器一大特点是,能把被装饰的函数替换成其他函数。第二大特性是,装饰器在加载模块时立刻执行。
7.2 Python何时执行装饰器
装饰器一个关键特性是,它们在被装饰的函数定义之后立刻执行。这通常是在导入时(即Python加载模块时),如示例7-2 中的registration.py
模块
示例7-2 registration.py模块
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('runnning f1()')
@register
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__ == '__main__':
main()
running register(<function f1 at 0x0000026691FB8700>)
running register(<function f2 at 0x0000026691FDF040>)
running main()
registry -> [<function f1 at 0x0000026691FB8700>, <function f2 at 0x0000026691FDF040>]
runnning f1()
running f2()
running f3()
注意 register 在模块中其他函数之前运行(两次)。调用 register 时,传给它的参数是被被装饰的参数,例如<function f1 at 0x0000026691FB8700>
。
加载模块后,registry 中有两个被装饰函数的引用:f1 和 f2。这两个参数,以及f3,只在 main 明确调用它们时才执行。
如果导入registration.py模块(不作为脚本运行),输出如下:
>>>import sentence
running register(<function f1 at 0x000002743505F040>)
running register(<function f2 at 0x000002743516AF70>)
示例7-2 主要想强调,函数装饰器在导入模块时立刻执行,而被装饰的函数只在明确调用时运行。这突出了Python程序员所说的 导入时 和 运行时 之间的区别。
考虑到装饰器在真实代码中的常用方式,示例7-2 有两个不寻常的地方。
- 装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装饰器通常在一个模块中定义,然后应用到其他模块的函数上。
- register 装饰器返回的函数与通过参数传入的相同。实际上,大多数装饰器会在内部定义一个函数,然后将其返回。
虽然示例7-2 中的register 装饰器原封不动地返回被装饰的函数,但是这种技术并非没有用处。很多Python Web 框架使用这样的装饰器把函数添加到某种中央注册处,例如把URL模式映射到生成HTTP响应的函数上的注册上处。这种注册装饰器可能会也可能不会修改被装饰的函数。
7.3 使用装饰器改进“策略”模式
回头看完第六章再说。
7.4 变量作用域规则
b = 6
def f2(a):
print(a)
print(b)
b = 9
f2(3)
Traceback (most recent call last):
File "E:/project/Getting Started/sentence.py", line 10, in <module>
f2(3)
File "E:/project/Getting Started/sentence.py", line 6, in f2
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
3
注意,首先输出了3,这表明了print(a)语句执行了但是第二个print(b)执行不了。
Python 编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本地环境获取b。后面调用f2(3)
时,f2的定义体获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值。
这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量时局部变量。
如果在函数中赋值时想让解释器把b当成全局变量,要使用 global 声明:
b = 6
def f3(a):
print(a)
global b
print(b)
b = 9
f3(3)
print(b)
3
6
9
7.5 闭包
人们常把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数并不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。因此很多人是同时知道这两个概念的。
其实,闭包指延伸了作用域的函数,其中包括:函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑到目前为止所有的价格。
起初,avg 是这样使用的:
初学者可能会像示例7-8那样使用类实现。
示例7-8 average_oo.py 计算移动平均值的类
class Average:
def __init__(self):
self.series = []
def __call__(self, price:float) -> float:
self.series.append(price)
return sum(self.series)/len(self.series)
Average 的 实例是可调用对象:
avg = Average()
print(avg(10))
print(avg(11))
print(avg(67))
10.0
10.5
29.333333333333332
示例7-9 函数式实现,使用高阶函数make_average。
def make_average():
series = []
def average(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return average
调用make_average 时,返回一个 average 函数对象。每次调用average 时,它会把参数添加到系列值中,然后计算当前平均值,如下:
avg = make_average()
print(avg(10))
print(avg(62))
print(avg(54))
注意,这两个示例有共同之处:调用Average()或 make_average()得到一个可调用对象avg,他会更新历史值,然后计算当前均值。在示例7-8中,avg 是 Average的实例;在示例7-9中是内部函数average。
示例7-9中在哪里寻找series呢?注意series是make_average函数的局部变量,因为那个函数的定义体中初始化了series:series = []
。可是,调用avg(10)时,make_averager
函数已经返回了,而它的本地作用域也一去不复返了。
在average 函数中,series 是 自由变量(free variable)。这是个技术术语,指未在本地作用域中绑定的变量。
average 的闭包延伸到那个函数的作用域之外,包括自由变量series的绑定。
审查返回的average对象,我们发现Python在_code_属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称,如下代码:
print(avg.__code__.co_varnames)
print(avg.__code__.co_freevars)
'''
('new_value', 'total')
('series',)
'''
series 的绑定在返回的avg函数的_closure_ 属性中。avg._closure_中的各个元素对应于avg.code.co_freevars中的一个名称。这些元素是cell对象,有个cell_contents属性,保存着真正的值。这些属性的值如示例7-12所示。
print(avg(10))
print(avg(62))
print(avg(54))
print(avg.__code__.co_freevars)
print(avg.__closure__)
print(avg.__closure__[0].cell_contents)
10.0
36.0
42.0
('series',)
(<cell at 0x000001E46BA398E0: list object at 0x000001E46BB5FE40>,)
[10, 62, 54]
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
7.6 nonlocal声明
前面实现make_average
函数的方法效率不高。在示例7-9中,我们把所有值存储在历史数组中,然后每次调用average时使用sum求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。
示例7-13 中的实现有缺陷,只是为了阐明观点。能看出缺陷在哪吗?
示例7-13 计算移动平均值的高阶函数,不保存所有历史值,但有缺陷
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager
这样是会报错的。UnboundLocalError: local variable 'count' referenced before assignment
问题是,当count 是数字或任何不可变类型时,count += 1
语句的作用其实与count = count +1
一样。因此,我们在averager 的定义体中为count 赋值了,这会把count 变成局部变量了。total 变量也是这个问题。
如果尝试重新绑定,例如count = count + 1
,其实会隐式创建局部变量count 。这样count就不是自由变量了,因此不会被保存在闭包中。
为了解决这个问题,Python3引入了nonlocal 声明。它的作用是把变量标记为自由变量,即便在函数中为变量赋值新值了,也会变成自由变量。如果为nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。更新版make_averager的正确实现如示例7-14所示。
示例7-14 计算移动平均值,不保存所有历史(使用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
avg = make_averager()
print(avg(10))
对付没有nonlocal的Python2
基本上,这种处理方式是把内部函数需要修改的变量存储为可变对象(字典或简单的实例)的元素或属性,并且把那个对象绑定给一个自由变量。
7.7 实现一个简单的装饰器
示例7-15 一个简单的装饰器,它会在每次调用把装饰的函数时倒计时,然后把经过的时间、传入的参数和调用的结果打印出来。
示例7-15 一个简单的装饰器,输出函数的运行时间
import time
def clock(func):
def clocked(*args): # 1 定义内部函数,接受任意个定位参数。
t0 = time.perf_counter()
result = func(*args) # 2 这行代码可用,是因为clocked的闭包包含自由变量func
elapsed = time.perf_counter()
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked # 3 返回内部函数,取代被装饰的函数
下面演示了clock装饰器的用法。
示例7-16 使用clock装饰器
import time
def clock(func):
def clocked(*args): # 1 定义内部函数,接受任意个定位参数。
t0 = time.perf_counter()
result = func(*args) # 2 这行代码可用,是因为clocked的闭包包含自由变量func
elapsed = time.perf_counter()
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked # 3 返回内部函数,取代被装饰的函数
@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))
**************************************** Calling snooze(.123)
[0.17904810s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.17919050s] factorial(1) -> 1
[0.17920040s] factorial(2) -> 2
[0.17920660s] factorial(3) -> 6
[0.17921170s] factorial(4) -> 24
[0.17921670s] factorial(5) -> 120
[0.17922200s] factorial(6) -> 720
6! = 720
工作原理
@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,然后函数返回clocked函数,Python解释器在背后会把clocked 赋值给 factorial。其实,导入clockdeco_demo 模块后查看factorail的_name_属性,会得到如下结果:
print(factorial.__name__)
#clocked
所以,现在factorial 保存的是clocked 函数的引用。自此之后,每次调用factorial(n),执行的都是clocked(n)。clocked大致做了下面几件事。
- 记录初始时间 t0
- 调用原来的factorial函数,保存结果。
- 计算经过的时间。
- 格式化收集的数据,然后打印出来。
- 返回第2步保存的结果。
这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外工作。
示例7-15中实现的clock装饰器有几个却带你:不支持关键字参数,而且遮盖了被装饰函数的_name_和_doc_属性。示例7-17使用functools.wraps
装饰器把相关的属性从func 复制到clocked 中。此外这个新版还能正确处理关键字参数。
示例7-17 改进后的clock装饰器
def clock(func):
@functools.wraps(func)
def clocked(*args,**kwargs):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
functools.wraps
只是标准库中拿来即用的装饰器之一。下一节将介绍functools 模块中最让人印象深刻的两个装饰器:lru_cache 和 singledispatch。
7.8 标准库中的装饰器
Python中内置了三个装饰方法的函数:property、classmethod
和staticmethod
。property在19章讨论,另外两个在9.4节讨论。
另一个常见的装饰器是functools.wraps,它的作用是协助构建行为良好的装饰器。我们在示例7-17中用过。标准库中最值得关注的两个装饰器是 lru_cache 和 全新的 singledispatch(Python3.4 新赠)。这两个装饰器都在functools 模块中定义。接下来分别讨论它们。
7.8.1 使用functools.lru_cache做备忘
functools.lru_cache实现了备忘功能。这是一项优化技术,把耗时的函数结果保存起来,避免传入相同的参数时重复计算。LRU三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间内不用的缓存条目会被扔掉。
生成第n个斐波那契数这种慢速递归函数适合使用lru_cache,如示例7-18 所示。
示例7-18 生成n个斐波那契数,递归方式非常耗时
@clock
def fibonacci(n):
if n < 2:
return n
else:
return fibonacci(n - 2) + fibonacci(n - 1)
运行得到的结果如下。除了最后一行,其余输出都是clock装饰器生成的。
[0.00000040s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00003430s] fibonacci(2) -> 1
[0.00000020s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000020s] fibonacci(1) -> 1
[0.00001050s] fibonacci(2) -> 1
[0.00002090s] fibonacci(3) -> 2
[0.00006540s] fibonacci(4) -> 3
[0.00000020s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000020s] fibonacci(1) -> 1
[0.00000980s] fibonacci(2) -> 1
[0.00001980s] fibonacci(3) -> 2
[0.00000020s] fibonacci(0) -> 0
[0.00000010s] fibonacci(1) -> 1
[0.00000990s] fibonacci(2) -> 1
[0.00000010s] fibonacci(1) -> 1
[0.00000020s] fibonacci(0) -> 0
[0.00000020s] fibonacci(1) -> 1
[0.00001060s] fibonacci(2) -> 1
[0.00002030s] fibonacci(3) -> 2
[0.00003980s] fibonacci(4) -> 3
[0.00006930s] fibonacci(5) -> 5
[0.00014470s] fibonacci(6) -> 8
8
浪费时间的地方很明显:fibonacci(1)调用了8次,fibonacci(2)调用了5次……但是,如果增加两行代码,使用lru_cache,性能会显著改善,如示例7-19所示。
示例7-19 使用缓存实现,速度更快
@functools.lru_cache()# 1
@clock # 2
def fibonacci(n):
if n < 2:
return n
else:
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
print(fibonacci(6))
- 注意,必须像常规函数那样调用lru_cache。这一行中有一对括号:
@functools.lru_cache()
。这么做的原因是,lru_cache 可以接受配置参数,稍后说明。 - 这里叠加了装饰器:
@functools.lru_cache()
应用到@clock
返回的函数上
这样依赖,执行时间减半了,而且n的每个值都只调用一次函数:
[0.00000040s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00003250s] fibonacci(2) -> 1
[0.00000050s] fibonacci(3) -> 2
[0.00004440s] fibonacci(4) -> 3
[0.00000030s] fibonacci(5) -> 5
[0.00005510s] fibonacci(6) -> 8
8
除了优化递归算法之外,lru_cache 在从Web中获取信息的应用中也能发挥巨大作用。
lru_cache可以使用两个可选的参数来配置。它的签名是:
functools.lru_cache(maxsize=128, typed=False)
maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该被设为2的幂。typed参数如果设为True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如1 和 1.0)区分开。顺便说一下,因为lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache 装饰的函数,它的所有参数都必须时可散列的。
接下来讨论吸引人的functools.singledispatch
装饰器。
7.8.2 单分派泛函数
假如我们在开发一个调试Web应用的工具,我们想生成HTML,显示不同类型的Python对象。编写的代码如下:
import html
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
这个函数适用于任何Python类型,但是现在想做个拓展,让它使用特别的方式显示某些类型。
- str:把内部的换行符替换为
<br\n>
;不使用<pre>
,而是使用<p>
- int:以十进制和十六进制显示数字。
- list:输出一个HTML列表,根据各个元素的类型进行格式化。
想要的行为如示例7-20 所示。
示例7-20 生成HTML的htmlize函数,调整了几种对象的输出
print(htmlize({1, 2, 3})) # 1
print(htmlize(abs)) # 2
print(htmlize('Heimlich & Co.\n- a game'))
print(htmlize(42)) # 3
print(htmlize(['alpha', 66, {3, 2, 1}])) # 4
<pre>{1, 2, 3}</pre>
<pre><built-in function abs></pre>
<p>Heimlich & Co.<br>
- a game</p>
<pre>42 (0x2a)</pre>
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
- 默认情况下,在
<pre></pre>
中显示HTML转义后的对象字符串表示形式。 - 为str 对象显示的也是HTML转义后的字符串表示形式,不过放在
<p></p>
中,而且使用<br>
表示换行。 - int 显示为十进制或十六进制两种形式,放在
<pre></pre>
中。 - 各个列表项目根据自己的类型格式化,整个列表则渲染成HTML 列表。
因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize 的变体,也无法使用不同的方式处理不同的数据类型。在Python中,一种常见的做法是把htmlize 变成一个分派函数,使用一串 if/elif/elif
,调用专门的函数,如htmlize_str
、htmlize_int
,等等。这样不便于模块的用户拓展,还显得笨拙;时间一长,分派函数htmlize 会变得很大,而且它和各个专门函数之间得耦合也很紧密。
新增的functools.singledispatch
装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用@singledispatch
装饰的普通函数会变成 泛函数(generic function):根据第一个参数的类型,以不同的方法执行相同操作的一组函数。具体做法参见示例7-21。
示例7-21 singledispatch 创建一个自定义的htmlize.register装饰器,把多个函数绑定到一起组成一个泛函数
import html
from functools import singledispatch
from collections import abc
import numbers
@singledispatch # 1
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str) # 2
def _(text): # 3
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral) # 4
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple) # 5
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
- 用
@singledispatch
标记处理object类型的基函数。 - 各个专门函数使用
@<base_function>.register(<type>)
装饰。 - 专门函数的名称无关紧要;_是一个不错的选择,简单明了。
- 为每个需要特殊处理的类型注册一个函数。
numbers.Integal
是 Int 的虚拟超类。 - 可以叠加多个register 装饰器,让同一个函数支持不同类型
只要可能,注册的专门函数应该处理抽象基类(如 numbers.Integral
和 abc.MutableSequence
),不要处理具体实现(int 和 list)。这样,代码支持的兼容类型更广泛。例如,Python扩展可以子类化numbers.Integral
,使用固定的位数实现int 类型。
使用抽象基类检查类型,可以让代码支持这些抽象基类现有和未来的具体子类或虚拟子类。抽象基类的作用和虚拟子类的概念在11章讨论。
singledispatch机制的一个显著类型是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。
singledispatch是经过深思熟虑才添加到标准库中的,提供的特性很多,这里无法一一说明。这个机制最好的文档是**“PEP 443 ——Single-dispatch generic functions”**。
@singledispatch
不是为了把Java的那种方法重载带入Python。这个类中为同一种方法定义多个重载变体,比在一个函数中使用一长串的if /elif/ elif
块要更好。但是这两种方案都有缺陷,因为它们让代码单元(类或函数)承担的职责太多。@singledispatch
的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数。
装饰器是函数,因此可以组合起来使用(即,可以在已经被装饰的函数上应用装饰器,如示例7-21所示)。下一步说明其中的原理。
7.9 叠放装饰器
把@d1
和@d2
两个装饰器按顺序应用到 f 函数上,作用相当于 f = d1(d2(f) )
.也就是说,下述代码:
@d1
@d2
def f():
print('f')
等同于:
def f():
print('f')
f = d1(d2(f))
除了叠放装饰器之外,本章还用到了几个接受参数的装饰器,例如@lru_cache()
和 示例7-21中 的@singledispatch
生成的htmlize.register(<type>)
。下一节说明如何构建接受参数的装饰器。
7.10 参数化装饰器
解析源码中的装饰器时,Python把被装饰的函数 作为第一个参数传给装饰器参数。那么怎么让装饰器接受其他的参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。
示例7-22 示例7-2 的模块缩减版,这里再次给出方便讲解。
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
print('running main()')
print('registry ->', registry)
f1()
7.10.1 一个参数化的注册装饰器
为了让register同时具备可选的注册和注销功能,需要设置一个参数,将其设为False时,不注册被装饰的函数。实现方式参加示例7-23.从概念上看,这个新的register函数不是装饰器,而是装饰器工厂函数。调用它会返回一个真正的装饰器,这才是应用到目标函数上的装饰器。
示例7-23 为了接受参数,新的register装饰器必须作为函数调用
import html
from functools import singledispatch
from collections import abc
import numbers
registry = set() # 1
def register(active=True): # 2
def decorate(func): # 3
print('running register(active=%s)->decorate(%s)'
% (active, func))
if active: # 4
registry.add(func)
else:
registry.discard(func) # 5
return func # 6
return decorate # 7
@register(active=False) # 8
def f1():
print('running f1()')
@register() # 9
def f2():
print('running f2()')
def f3():
print('running f3()')
print(registry)
- registry 是一个set对象,这样添加或删除元素比较快。
- register 接受一个可选的关键字参数。
- decorate 这个内部函数是真正的装饰器;注意它的参数是一个函数。
- 只有active 是True 时才注册func。
- 如果active 不为真,而且func 在 registry 中,那么把它删除。
- decorate 是装饰器,必须返回一个函数。
- registry 是装饰器共海沧函数,因此返回decorate。
@registry
工厂函数必须作为函数调用,并且传入所需的参数。- 即使不传入参数,register 也必须作为函数调用
(@register())
,即要返回真正的装饰器。
@这个式子必须要接受参数func ,也就是被装饰的函数,所以如果外层参数不是func ,就要()调用,进入里层函数。关键:register()要返回decorate,然后把它应用到被装饰的函数上。
运行如下:
running register(active=False)->decorate(<function f1 at 0x0000020BEB6354C0>)
running register(active=True)->decorate(<function f2 at 0x0000020BEB635550>)
{<function f2 at 0x0000020BEB635550>}
只有f2 函数在registry 中;f1 不在其中,因为传给registry 装饰器工厂函数的参数是active=Flase,所以应用到f1 上 的decorate 没有把它添加到registry 中。
如果不使用@句法,那就要像常规函数那样使用register ;若想把f添加到registry 中,则装饰f函数的句法是register()(f)
;不想添加的话,句法是register(active=False)(f)
。示例7-24演示了如何把函数添加到 registry 中,以及如何从中删除函数。
示例7-24 使用示例7-23
register()(f3)
register(active=False)(f2)
第一行可以登记f3 函数,第二行删除f2 函数。
参数化装饰器的原理相当复杂,参数化装饰器通常会把被装饰的函数替换掉,而且结构上需要多一层嵌套。接下来讨论这种函数金字塔。
7.10.2 参数化clock装饰器
再次讨论clock参数器,为它添加一个功能:让用户传入一个格式字符串,控制被装饰函数的输出。参见7-25.
示例7-25 参数化clock装饰器
import html
from functools import singledispatch
from collections import abc
import numbers
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): # 1
def decorate(func): # 2
def clocked(*_args): # 3
t0 = time.perf_counter()
_result = func(*_args) # 4
elapsed = time.perf_counter()
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) # 5
result = repr(_result) # 6
print(fmt.format(**locals())) # 7
return _result # 8
return clocked # 9
return decorate # 10
if __name__ == '__main__':
@clock() # 11
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(),因此应用的装饰器使用默认的格式str。
运行结果如下:
[0.20810760s] snooze(0.123) -> None
[0.33423020s] snooze(0.123) -> None
[0.46081770s] snooze(0.123) -> None
# 这个是被装饰的函数外的内容,都会在导入时运行,
def clock(*_args):
# do something
# 这里会在调用被装饰的函数时运行,也就是运行时。
7.11 本章小结
@functools.wraps(func)
用在func下面,获取参数上面,用于复制被装饰的函数信息。
7.12 延伸阅读
《Python Cookbook》