函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。
7.1 装饰器的基础知识
装饰器是可调用对象,其参数是另一个函数。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另外一个函数或可调用对象。
@decorate
def target():
print('running target()')
上述代码的效果与下述写法一样:
def target():
print('running target()')
target = decorate(target)
示例7-1 装饰器通常把函数替换成另一个函数
>>> def deco(func):
... def inner():
... print('running inner()')
... return inner
...
>>> @deco
... def target():
... print('running target()')
...
>>> target()
running inner()
>>> target
<function deco.<locals>.inner at 0x0000013E572A2510>
严格来说,装饰器只是语法糖。
装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。
7.2 Python何时执行装饰器
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时。
示例7-2 registration.py模块
# encoding:utf-8
registry = []
def register(func):
print('runing register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running 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()
输出如下:
runing register(<function f1 at 0x000002073FD85730>)
runing register(<function f2 at 0x0000020750562510>)
running main()
registry -> [<function f1 at 0x000002073FD85730>, <function f2 at 0x0000020750562510>]
running f1()
running f2()
running f3()
示例7-2主要想强调,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这突出了Python程序员所说的导入时与运行时之间的区别。
示例7-3 promos列表中的值使用promotion装饰器填充
# encoding:utf-8
promos = []
def promotion(promo_func):
promos.append(promo_func)
return promo_func
@promotion
def fidelity(order):
"""为积分为1000或以上的顾客提供5%折扣"""
return order.total() * .5 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item(order):
"""单个商品为20个或以上时提供10%的折扣"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
@promotion
def large_order(order):
"""订单中的不同商品达到10个或以上时提供7%的折扣"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
def best_promo(order):
"""选择可用的最佳折扣"""
return max(promo(order) for promo in promos)
7.4 变量作用域规则
看一个可能会让你吃惊的例子
>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
Python编译函数的定义体时,它判断b是局部变量,因为在函数中给赋值了。生成的字节码证实了这种判断,Python会尝试从本地环境获取b。后面调用f2(3)时,f2的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的时候,发现b没有绑定值。
这不是缺陷,而是设计选择:Python不要求声明变量,但是假定在函数定义提中赋值的变量是局部变量。
如果在函数中赋值时想让解释器把b当成全局变量,要使用global声明:
>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9
>>> f3(3)
3
9
>>> b = 30
>>> b
30
7.5 闭包( Closure)
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。
示例7-9 average:计算移动平均值的高阶函数
>>> def make_averager():
... series = []
... def averager(new_value):
... series.append(new_value)
... total = sum(series)
... return total / len(series)
... return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
在averager函数中,series是自由变量。
简单来说:闭包=函数+自由变量的引用。
>>> avg.__code__.co_varnames # 局部变量
('new_value', 'total')
>>> avg.__code__.co_freevars # 自由变量
('series',)
>>> avg.__closure__ # series的绑定在返回的avg函数的__closure__属性中
(<cell at 0x7fbaa00aad08: list object at 0x7fba80034548>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
7.6 nonlocal声明
前面实现make_averager函数的方法效率不高。我们把所有值存储在历史数列中,然后在每次调用averager时使用sum求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个值计算均值。
>>> def make_averager():
... count = 0
... total = 0
... def averager(new_value):
... count += 1
... total += new_value
... return total / count
... return averager
...
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in averager
UnboundLocalError: local variable 'count' referenced before assignment
当count是数字或任何不可变类型时, count += 1 语句的作用其实与 count = count + 1一样。因此,我们在averager的定义体中为count赋值了,这会把count变成局部变量。
为了解决这个问题,Python3中引入了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()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
7.7 实现一个简单的装饰器
示例7-15 一个简单的装饰器,输出函数的运行时间
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
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
示例7-16 使用clock装饰器
import time
from clockdeco 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)')
factorial(6)
运行结果如下:
**************************************** Calling snooze(.123)
[0.12711179s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000054s] factorial(1) -> 1
[0.00021925s] factorial(2) -> 2
[0.00023042s] factorial(3) -> 6
[0.00023575s] factorial(4) -> 24
[0.00024129s] factorial(5) -> 120
[0.00024796s] factorial(6) -> 720
7.8 标准库中的装饰器
Python内种了三个用于装饰方法的函数:property、classmethod和staticmethod。
另一个常见的装饰器是functools.wraps。标准库中最值得关注的两个装饰器是lru_cache和singledispatch。
7.8.1 使用functools.lru_cache做备忘
它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。
示例7-13 生成n个斐波那契数,递归方式非常耗时
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))
运行结果如下:
[0.00000033s] fibonacci(0) -> 0
[0.00000063s] fibonacci(1) -> 1
[0.00059033s] fibonacci(2) -> 1
[0.00000029s] fibonacci(1) -> 1
[0.00000029s] fibonacci(0) -> 0
[0.00000025s] fibonacci(1) -> 1
[0.00000675s] fibonacci(2) -> 1
[0.00001313s] fibonacci(3) -> 2
[0.00061025s] fibonacci(4) -> 3
[0.00000025s] fibonacci(1) -> 1
[0.00000021s] fibonacci(0) -> 0
[0.00000025s] fibonacci(1) -> 1
[0.00000600s] fibonacci(2) -> 1
[0.00001208s] fibonacci(3) -> 2
[0.00000021s] fibonacci(0) -> 0
[0.00000021s] fibonacci(1) -> 1
[0.00000608s] fibonacci(2) -> 1
[0.00000021s] fibonacci(1) -> 1
[0.00000025s] fibonacci(0) -> 0
[0.00000025s] fibonacci(1) -> 1
[0.00000637s] fibonacci(2) -> 1
[0.00001221s] fibonacci(3) -> 2
[0.00002754s] fibonacci(4) -> 3
[0.00004517s] fibonacci(5) -> 5
[0.00066204s] fibonacci(6) -> 8
8
示例7-19 使用缓存实现,速度更快
import functools
from clockdeco import clock
@functools.lru_cache() # 有括号
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__ == "__main__":
print(fibonacci(6))
执行结果:
[0.00000042s] fibonacci(0) -> 0
[0.00000058s] fibonacci(1) -> 1
[0.00060767s] fibonacci(2) -> 1
[0.00000079s] fibonacci(3) -> 2
[0.00061683s] fibonacci(4) -> 3
[0.00000054s] fibonacci(5) -> 5
[0.00062571s] fibonacci(6) -> 8
8
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列表,根据各个元素的类型进行格式化。
因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize的变体,也无法使用不同的方式处理不同的数据类型。在Python中,一种常见的做法是把htmlize变成一个分派函数,使用一串if/elif/elif,调用专门的函数,如htmlize_str、htmlize_int等等。这样不便于模块的用户扩展,还显得笨拙,时间一长,分派函数htmlize会变得很大,而且它与各个专门函数之间的耦合也很紧密。
Python3.4新增的functools.singledispatch装饰器可以把整个方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用@singledispatch装饰的普通函数会变成泛函数:根据第一个参数的类型,以不同方式执行相同操作的一组函数。
示例7-21 singledispatch创建一个自定义的htmlize.register装饰器,把多个函数绑在一起组成一个泛函数。
>>> from functools import singledispatch
>>> from collections import abc
>>> import numbers
>>> import html
>>> @singledispatch
... def htmlize(obj):
... content = html.escape(repr(obj))
... return '<pre>{}</pre>'.format(content)
...
>>> @htmlize.register(str)
... def _(text):
... content = html.escape(text).replace('\n', '<br>\n')
... return '<p>{0}</p>'.format(content)
...
>>> @htmlize.register(numbers.Integral)
... def _(n):
... return '<pre>{0} (0x{0:x})</pre>'.format(n)
...
>>> @htmlize.register(tuple)
... @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>'
...
测试
>>> htmlize({1,2,3})
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre><built-in function abs></pre>'
>>> htmlize('Heimlich a game')
'<p>Heimlich a game</p>'
>>> htmlize('Heimlich &Co.\n a game')
'<p>Heimlich &Co.<br>\n a game</p>'
>>> htmlize(42)
'<pre>42 (0x2a)</pre>'
>>> print(htmlize(['alpha', 66, {3, 2, 1}]))
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x(0:x))</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
7.9 叠放装饰器
@d1
@d2
def f():
print('f')
等同于
def f():
print('f')
f = d1(d2(f))
7.10 参数化装饰器
Python把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器。
registry = []
def register(func): # 这是一个最简单形式的装饰器,参数函数,返回值也是函数
print('runing register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
print('running main()')
print('registry ->', registry)
f1()
7.10.1 一个参数化的注册装饰器
registry = set()
>>> def register(active=True):
... def decorate(func):
... print('running register(active=%s)->decorate(%s)'%(active, func))
... if active:
... registry.add(func)
... else:
... registry.discard(func)
... return func
... return decorate
...
>>> @register(active=False)
... def fl():
... print('running fl()')
...
running register(active=False)->decorate(<function fl at 0x7fba903c1f28>)
>>> @register()
... def f2():
... print('running f2()')
...
running register(active=True)->decorate(<function f2 at 0x7fba902a92f0>)
>>> def f3():
... print('running f3()')
...