装饰器基础知识
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
假如有个名为 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 inner()
严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。
综上,装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。
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