装饰器基础知识
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
假如有个名为 decorate 的装饰器:
@decorate
def target():
pprint('running target()')
上述代码的效果与下述写法一样:
def target():
print('running target()')
target = decorate(target)
两种写法的最终结果一样:上述两个代码片段执行完毕后得到的target 不一定是原来那个 target 函数,而是 decorate(target) 返回的函数
举个🌰 装饰器通常把函数替换成另一个函数
1 def deco(func):
2 def inner():
3 print('running in inner()')
4 return inner
5
6 @deco
7 def target():
8 print('running in target()')
9
10 target()
以上代码执行的结果为:
running in
严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。
装饰器在加载模块时立即执行。
Python何时执行装饰器
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时),如 🌰 的registration.py 模块所示。
1 registry = []
2
3 def register(func):
4 '''
5 :param func:被装饰器的函数
6 :return: 返回被装饰的函数func
7 '''
8 print('running register(%s)' % func) #获取形参func的的引用
9 registry.append(func) #获取的引用地址放入到类表中
10 return func #执行装饰器的时候返回func
11
12 @register
13 def f1():
14 print('running f1()')
15
16 @register
17 def f2():
18 print('running f2()')
19
20 def f3():
21 print('running f3()')
22
23 def main():
24 print('running main()')
25 print('registry ->', registry)
26 f1()
27 f2()
28 f3()
29
30 if __name__ == "__main__":
31 main()
以上代码执行的结果为:
running register(<function f1 at 0x101c7bf28>)
running register(<function f2 at 0x101c83048>)
running main()
registry -> [<function f1 at 0x101c7bf28>, <function f2 at 0x101c83048>]
running f1()
running f2()
running f3()
注意,register 在模块中其他函数之前运行(两次)。调用register 时,传给它的参数是被装饰的函数,例如 <function f1 at 0x101c7bf28>。
加载模块后,registry 中有两个被装饰函数的引用:f1 和 f2。这两个函数,以及 f3,只在 main 明确调用它们时才执行。
使用装饰器改进“策略”模式
使用注册装饰器可以改进的电商促销折扣 🌰 回顾一下,函数策略主要问题是,定义体中有函数的名称,但是best_promo 用来判断哪个折扣幅度最大的 promos 列表中也有函数名称。这种重复是个问题,因为新增策略函数后可能会忘记把它添加到promos 列表中,导致 best_promo 忽略新策略,而且不报错,为系统引入了不易察觉的缺陷。下面的 🌰 使用注册装饰器解决了这个问题。
🌰 promos 列表中的值使用 promotion 装饰器填充
1 promos = []
2
3 def promotion(promo_func):
4 promos.append(promo_func)
5 return promo_func
6
7 @promotion
8 def fidelity(order):
9 """为积分为1000或以上的顾客提供5%折扣"""
10 return order.total() * .05 if order.customer.fidelity >= 1000 else 0
11
12 @promotion
13 def bulk_item(order):
14 """单个商品为20个或以上时提供10%折扣"""
15 discount = 0
16 for item in order.cart:
17 if item.quantity >= 20:
18 discount += item.total() * .1
19 return discount
20
21 @promotion
22 def large_order(order):
23 """订单中的不同商品达到10个或以上时提供7%折扣"""
24 distinct_items = {item.product for item in order.cart}
25 if len(distinct_items) >= 10:
26 return order.total() * .07
27 return 0
28
29 def best_promo(order):
30 """选择可用的最佳折扣"""
31 return max(promo(order) for promo in promos)
与函数策略给出的方案相比,这个方案有几个优点:
- @promotion装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略,只需要把装饰器注释掉
- 促销折扣策略可以在其他的模块中定义,在系统中的任何地方都行,只要使用@promotion装即可
变量作用域规则
举个 🌰 来说明函数作用域的问题,我们定义并测试一个函数,它读取两个两个变量的值:一个是局部变量a,是函数的参数;另外一个变量b,这个函数没有定义它!
>>> 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: name 'b' is not defined
如果先给全局变量 b 赋值,然后再调用 f,那就不会出错:
>>> b = 10
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(50)
50
10
来个让你大吃一惊的 🌰 在函数的内部直接修改全局变量b,我们看下会出现什么问题
>>> b = 10
>>> def f1(a):
... print('f1 a:', a)
... print('f1 b:', b)
... b = 3
...
>>> f1(3)
f1 a: 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
UnboundLocalError: local variable 'b' referenced before assignment
注意,首先输出了 3,这表明 print(a) 语句执行了。但是第二个语句print(b) 执行不了。一开始我很吃惊,我觉得会打印 10,因为有个全局变量 b,而且是在 print(b) 之后为局部变量 b 赋值的。
可事实是,Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。后面调用 f1(3) 时, f1 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。
如何解决上面的问题呢,如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:
>>> b = 10
>>> def f1(a):
... global b
... print('f1 a:', a)
... print('f1 b:', b)
... b = 20
... print('f1 更改b值以后:', b)
...
>>> print('全局的b值', b)
全局的b值 10
>>> f1(3)
f1 a: 3
f1 b: 10
f1 更改b值以后: 20
>>> print(b)
20
闭包
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
需求:假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。
🌰 Low B版本的写法~
1 class Averager():
2
3 def __init__(self):
4 self.series = []
5
6 def __call__(self, new_value): #提供实例化以后可以直接通过传递值直接调用
7 self.series.append(new_value)
8 total = sum(self.series)
9 return total/len(self.series)
10
11 avg = Averager()
12 print(avg(10))
13 print(avg(11))
14 print(avg(12))
以上代码执行的结果为:
10.0
10.5
11.0
函数式实现的方式:
1 def make_averager():
2 series = []
3
4 def avgerager(new_value):
5 series.append(new_value)
6 total = sum(series)
7 return total/len(series)
8
9 return avgerager
10
11 avg = make_averager() #返回内部的avgerager函数
12 print(avg(10))
13 print(avg(11))
14 print(avg(12))
调用 make_averager 时,返回一个 averager 函数对象。每次调用averager 时,它会把参数添加到系列值中,然后计算当前平均值。
在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量,参见下图
averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定
闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
nonlocal声明
前面实现 make_averager 函数的方法效率不高。在示例中,我们把所有值存储在历史数列中,然后在每次调用 averager 时使用 sum 求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。
🌰 计算移动平均值的高阶函数,不保存所有历史值,但有缺陷
1 def make_averager():
2 count = 0
3 total = 0
4
5 def averager(new_value):
6 count += 1
7 total += new_value
8 return total / count
9
10 return averager
11
12 avg = make_averager()
13 avg(10)
上面的 🌰 报错了~,因为啥呢,往下面看
Traceback (most recent call last):
.........
.........
count += 1
UnboundLocalError: local variable 'count' referenced before assignment
问题是,当 count 是数字或任何不可变类型时,count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。total 变量也受这个问题影响。
为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。最新版 make_averager 的正确实现如示例:
🌰 计算移动平均值,不保存所有历史(使用 nonlocal 修正)
1 def make_averager():
2 count = 0
3 total = 0
4
5 def averager(new_value):
6 nonlocal count, total #类似于之前的global的用法,对于闭包的函数内部变量来说,修外层的函数作用域引发的问题,通过nonlocal来解决
7 count += 1
8 total += new_value
9 return total / count
10
11 return averager
12
13 avg = make_averager()
14 print(avg(10))
15 print(avg(11))
16 print(avg(12))
以上代码直接的结果为:
10.0
10.5
11.0
实现一个简单的装饰器
定义了一个装饰器,它会在每次调用被装饰的函数时计时,然后把经过的时间、传入的参数和调用的结果打印出来。
🌰 一个简单的装饰器,输出函数的运行时间
1 import time
2 from functools import wraps
3
4
5 def clock(func):
6 @wraps(func)
7 def clocked(*args):
8 t0 = time.perf_counter() #启动时间
9 result = func(*args) #调用被装饰的函数
10 elapsed = time.perf_counter() - t0 #获取被调用函数花费了多久
11 name = func.__name__ #获取被调用函数的函数名
12 arg_str = ', '.join(repr(arg) for arg in args) #格式化字符串拼接
13 print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
14 return result #返回被装饰函数运行的结果
15
16 return clocked #返回内部的装饰器函数
17
18 @clock #等同于snooze = clock(snooze)
19 def snooze(seconds):
20 time.sleep(seconds)
21
22 @clock
23 def factorial(n):
24 return 1 if n < 2 else n*factorial(n-1)
25
26
27 if __name__ == '__main__':
28 print('*' * 40, 'Calling snooze(.123)')
29 snooze(.123)
30 print('*' * 40, 'Calling factorial(6)')
31 print('6! =', factorial(6))
以上代码执行的结果为:
**************************************** Calling snooze(.123)
[0.12482605s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000110s] factorial(1) -> 1
[0.00002436s] factorial(2) -> 2
[0.00003814s] factorial(3) -> 6
[0.00004967s] factorial(4) -> 24
[0.00006173s] factorial(5) -> 120
[0.00007719s] 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。然后, clock 函数会返回 clocked 函数,Python 解释器在背后会把 clocked 赋值给 factorial。其实,导入clockdeco_demo 模块后查看 factorial 的 __name__ 属性,会得到如下结果:
>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>
所以,现在 factorial 保存的是 clocked 函数的引用。自此之后,每次调用 factorial(n),执行的都是 clocked(n)。clocked 大致做了下面几件事。
- 记录初始化时间t0
- 调用原来的factorial函数,保存结果
- 计算经过的时间
- 格式化手机的数据,然后打印出来
- 返回第2步保存的结果
这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。
下面的 🌰 使用functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中。此外,这个新版还能正确处理关键字参数。
1 def clock(func):
2 @wraps(func)
3 def clocked(*args, **kwargs):
4 t0 = time.time()
5 result = func(*args, **kwargs)
6 elapsed = time.time() - t0
7 name = func.__name__
8 arg_list = []
9 if args:
10 arg_list.append(', '.join(repr(arg) for arg in args))
11 if kwargs:
12 pairs = ['%s=%s' % (k, v) for k, v in sorted(kwargs.items())]
13 arg_list.append(','.join(pairs))
14 arg_str = ','.join(arg_list)
15 print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
16 return result
17
18 return clocked
标准库中的装饰器
Python 内置了三个用于装饰方法的函数:property、classmethod 和staticmethod。另一个常见的装饰器是 functools.wraps,它的作用是协助构建行为良好的装饰器。标准库中最值得关注的两个装饰器是 lru_cache 和全新的 singledispatch(Python 3.4 新增)。这两个装饰器都在 functools 模块中定义。接下来分别讨论它们。
使用functools.lru_cache做备忘
functools.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。LRU 三个字母是“LeastRecently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。
生成第 n 个斐波纳契数这种慢速递归函数适合使用 lru_cache
举个🌰 生成第 n 个斐波纳契数,递归方式非常耗时
1 from functools import wraps
2 import time
3
4
5 def clock(func):
6 @wraps(func)
7 def clocked(*args, **kwargs):
8 t0 = time.time()
9 result = func(*args, **kwargs)
10 elapsed = time.time() - t0
11 name = func.__name__
12 arg_list = []
13 if args:
14 arg_list.append(', '.join(repr(arg) for arg in args))
15 if kwargs:
16 pairs = ['%s=%s' % (k, v) for k, v in sorted(kwargs.items())]
17 arg_list.append(','.join(pairs))
18 arg_str = ','.join(arg_list)
19 print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
20 return result
21
22 return clocked
23
24 @clock
25 def fibonacci(n):
26 if n < 2:
27 return n
28 return fibonacci(n-1) + fibonacci(n-2)
29
30 if __name__ == "__main__":
31 print(fibonacci(6))
以上代码执行的结果为:
[0.00000000s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00006294s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00007701s] fibonacci(3) -> 2
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00001192s] fibonacci(2) -> 1
[0.00010014s] fibonacci(4) -> 3
[0.00000095s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00001192s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00002360s] fibonacci(3) -> 2
[0.00013614s] fibonacci(5) -> 5
[0.00000000s] fibonacci(1) -> 1
[0.00000381s] fibonacci(0) -> 0
[0.00059795s] fibonacci(2) -> 1
[0.00000191s] fibonacci(1) -> 1
[0.00063992s] fibonacci(3) -> 2
[0.00000095s] fibonacci(1) -> 1
[0.00000119s] fibonacci(0) -> 0
[0.00004625s] fibonacci(2) -> 1
[0.00071788s] fibonacci(4) -> 3
[0.00087523s] fibonacci(6) -> 8
8
浪费时间的地方很明显:fibonacci(1) 调用了 8 次,fibonacci(2)调用了 5 次……但是,如果增加两行代码,使用 lru_cache,性能会显著改善。
举个🌰 使用缓存实现,速度更快
1 from functools import wraps, lru_cache
2 import time
3
4
5 def clock(func):
6 @wraps(func)
7 def clocked(*args, **kwargs):
8 t0 = time.time()
9 result = func(*args, **kwargs)
10 elapsed = time.time() - t0
11 name = func.__name__
12 arg_list = []
13 if args:
14 arg_list.append(', '.join(repr(arg) for arg in args))
15 if kwargs:
16 pairs = ['%s=%s' % (k, v) for k, v in sorted(kwargs.items())]
17 arg_list.append(','.join(pairs))
18 arg_str = ','.join(arg_list)
19 print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
20 return result
21
22 return clocked
23
24 @lru_cache()
25 @clock
26 def fibonacci(n):
27 if n < 2:
28 return n
29 return fibonacci(n-1) + fibonacci(n-2)
30
31
32 if __name__ == "__main__":
33 print(fibonacci(6))
以上代码执行的结果为:
[0.00000095s] fibonacci(1) -> 1
[0.00000072s] fibonacci(0) -> 0
[0.00007081s] fibonacci(2) -> 1
[0.00008035s] fibonacci(3) -> 2
[0.00008821s] fibonacci(4) -> 3
[0.00009608s] fibonacci(5) -> 5
[0.00010514s] fibonacci(6) -> 8
8
特别要注意,lru_cache 可以使用两个可选的参数来配置。它的签名是:
functools.lru_cache(maxsize=128, typed=False)
maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。顺便说一下,因为 lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。
单分派泛函数
假设我们在开发一个调试 Web 应用的工具,我们想生成 HTML,显示不同类型的 Python 对象
我们可能会编写这样的函数:
1 from functools import singledispatch
2 from collections import abc
3 import numbers
4 import html
5
6
7 @singledispatch
8 def htmlize(obj):
9 content = html.escape(repr(obj))
10 return '<pre>{}</pre>'.format(content)
11
12 @htmlize.register(str)
13 def _(text):
14 content = html.escape(text).replace('\n', '<br>\n')
15 return '<p>{0}</p>'.format(content)
16
17 @htmlize.register(numbers.Integral)
18 def _(n):
19 return '<pre>{0} (0x{0:x})</pre>'.format(n)
20
21 @htmlize.register(tuple)
22 @htmlize.register(abc.MutableSequence)
23 def _(seq):
24 inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
25 return '<ul>\n<li>' + inner + '</li>\n</ul>'
26
27
28 print(htmlize({1, 2, 3}))
29 print('-'*50)
30 print(htmlize(abs))
31 print('-'*50)
32 print(htmlize('Heimlich & Co.\n- a game'))
33 print('-'*50)
34 print(htmlize(['alpha', 66, {3, 2, 1}]))
以上代码执行的结果为:
<pre>{1, 2, 3}</pre>
--------------------------------------------------
<pre><built-in function abs></pre>
--------------------------------------------------
<p>Heimlich & Co.<br>
- a game</p>
--------------------------------------------------
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
叠放装饰器
把 @d1 和 @d2 两个装饰器按顺序应用到 f 函数上,作用相当于 f =d1(d2(f))。
@d1
@d2
def f():
print('f')
等同于
def f():
print('f')
f = d1(d2(f))
参数化装饰器
解析源码中的装饰器时,Python 把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。不明白什么意思?当然。下面以我们见过的最简单的装饰器为例说明:
registration.py 模块的删减版,这里再次给出是为了便于讲解
1 registry = []
2
3 def register(func):
4 print('running register(%s)' % func)
5 registry.append(func)
6 return func
7
8 @register
9 def f1():
10 print('running f1()')
11
12 print('running main()')
13 print('registry ->', registry)
14 f1()
为了便于启用或禁用 register 执行的函数注册功能,我们为它提供一个可选的 active 参数,设为 False 时,不注册被装饰的函数。实现方式参见下面 🌰 。从概念上看,这个新的 register 函数不是装饰器,而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用到目标函数上的装饰器。
为了接受参数,新的 register 装饰器必须作为函数调用
1 registry = set() #创建一个空集合,用存放删除函数
2 def register(active=True):
3 def decorate(func): #真正的装饰器
4 print('running register(active=%s)->decorate(%s)' % (active, func))
5 if active: #注册的为真
6 registry.add(func) #把被装饰的函数添加到集合中
7 else:
8 registry.discard(func) #删除集合中的函数
9 return func #返回被装饰的函数
10
11 return decorate
12
13 @register(active=False)
14 def f1():
15 print('running f1()')
16
17 @register()
18 def f2():
19 print('running f2()')
20
21 def f3():
22 print('running f3()')
23
24 def main():
25 f1()
26 f2()
27 f3()
28
29 main()
以上代码执行的结果为:
running register(active=False)->decorate(<function f1 at 0x10147bf28>)
running register(active=True)->decorate(<function f2 at 0x101483048>)
running f1()
running f2()
running f3()
工作原理:
@register(active=False)
def f1():
print('running f1()')
等同于:
def f1():
print('running f1()')
f1 = register(active=True)(f1)
这里的关键是,register() 要返回 decorate,然后把它应用到被装饰的函数上。
参数化clock装饰器
clock 装饰器,为它添加一个功能:让用户传入一个格式字符串,控制被装饰函数的输出
1 from functools import wraps
2 import time
3
4 DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
5
6
7 def clock(fmt=DEFAULT_FMT):
8 def decorate(func):
9 @wraps(func)
10 def clocked(*_args):
11 t0 = time.time()
12 _result = func(*_args)
13 elapsed = time.time() - t0
14 name = func.__name__
15 args = ', '.join(repr(arg) for arg in _args)
16 result = repr(_result)
17 print(fmt.format(**locals()))
18 return _result
19 return clocked
20 return decorate
21
22
23 if __name__ == "__main__":
24 @clock()
25 def snooze(seconds):
26 time.sleep(seconds)
27
28 @clock('{name}: {elapsed}s')
29 def snooze1(seconds):
30 time.sleep(seconds)
31
32 @clock('{name}({args}) dt={elapsed:0.3f}s')
33 def snooze2(seconds):
34 time.sleep(seconds)
35
36 pool = [snooze, snooze1, snooze2]
37
38 for obj in pool:
39 for i in range(3):
40 obj(.123)
以上代码执行的结果为:
[0.12584805s] snooze(0.123) -> None
[0.12616611s] snooze(0.123) -> None
[0.12748885s] snooze(0.123) -> None
snooze1: 0.12738299369812012s
snooze1: 0.127885103225708s
snooze1: 0.12749981880187988s
snooze2(0.123) dt=0.128s
snooze2(0.123) dt=0.127s
snooze2(0.123) dt=0.127s