本章的最终目标是介绍清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的参数化装饰器。
7.1 装饰器基础知识
装饰器就是函数,给另一个函数装饰的,会处理被装饰的函数,然后把它返回,返回的函数可能会被替换另外一个函数或对象。
它的两大特性:
- 能把被装饰的函数替换成其他函数。
- 装饰器在加载模块时立即执行。
7.2 Python何时执行装饰器
函数装饰器在导入模块时立即执行,而被装饰器函数只在调用时运行。
7.3 使用装饰器改进“策略”模式
#利用装饰器在加载模块时,立即执行的特性,将被装饰的函数添加到promos列表中
promos = []
def promotion(promo_func):
promos.append(promo_func)
return promo_func
@promotion #被@promotion装饰的函数都会添加到promos列表中
def fidelity_promo(order):
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item_promo(order):
"""10% discount for each LineItem with 20 or more units"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
@promotion
def large_order_promo(order):
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
def best_promo(order):
"""Select best discount available
"""
return max(promo(order) for promo in promos)
7.4 变量作用域规则
局部变量和全局变量
7.5 闭包
只有当嵌套函数中才会需要处理外部变量,且这个外部变量不是全局变量,这时才会用到闭包。
闭包是一种函数,它会保留定义函数时存在的自由变量的绑定。自由变量,指未在本地作用域中绑定的变量。
#例子说明
#测试示例
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
7.6 nonlocal声明
前面实现make_averager函数的方法效率不高。在多次调用函数make_averager,将10、11、12都存放在列表中,再求平均值。更好的实现方式是,只存储目前的总值和元素个数,再计算两个数平均值。
#计算移动平均值的高阶函数,不保留所有历史值,但有缺陷
>>> 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 "<pyshell#33>", line 1, in <module>
avg(10)
File "<pyshell#31>", line 5, in averager
count += 1
UnboundLocalError: local variable 'count' referenced before assignment
保存原因:当count是数字或任何不可变类型时,count += 1中的count会被赋值,是一个局部变量,这样就不能保留在闭包里。而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
7.7 实现一个简单的装饰器
#一个简单的装饰器,输出函数的运行时间
#clockdeco.py
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.8f] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
#使用clock装饰器
# clockdeco_demo.py
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)')
print('6! =', factorial(6))
运行结果:
**************************************** Calling snooze(.123)
[0.12277146] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000342] factorial(1) -> 1
[0.00006899] factorial(2) -> 2
[0.00011832] factorial(3) -> 6
[0.00016308] factorial(4) -> 24
[0.00020299] factorial(5) -> 120
[0.00024718] factorial(6) -> 720
6! = 720
上述示例有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的__name__和__doc__属性,使用functools.wraps装饰器可以把相关属性从func复制到clocked中,还能处理关键字参数。
# clockdeco2.py
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 = []
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
7.8 标准库的装饰器
接下来,我们重点介绍functools模块中的两个装饰器:lru_cache和singledispath。
使用functools.lru_cache做备忘
特点:可以把耗时的函数结果保存起来,避免传入相同的参数时重复计算。
#使用缓存实现,速度更快
import functools
from clockdeco import clock
@functools.lru_cache() #lru_cache可以加参数,后面要加括号
@clock #这里放了叠加装饰器:@lru_cache()应用到@clock返回的函数上
def fibonacci(n):
if n < 2
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
print(fibonacci(6))
lru_cache可以使用两个可选的参数来配置。它的签名是:
functools.lru_cache(maxsize=128, typed=False)
maxsize参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize应该设为2的幂。typed参数如果设为True,把不同参数类型得到的结果分开保存,即把通常认为相等的(1.0和1)区分开。顺便说一下,因为lru_cache使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数必须是可散列的。
单分派泛函数
假设我们再开发一个调试Web应用的工具,我们想生存HTML,显示不同类型的Python对象。但Python不支持重载方法或函数,所以我们会使用到functools.singledispatch装饰器,将普通函数变成泛函数。
#singledispatch创建一个自定义的htmlize.register装饰器,把多个函数绑在一起组成一个泛函数。
from functools import singledispatch
from collection import abc
import numbers
import html
@singledispatch #@singledispatch标记出来object类型的基函数
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str) #各个专门函数使用@htmlize.register装饰
def _(text): #专门函数的名称无关紧要;_是个不错的选择
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral) #为每个需要特殊处理的类型注册一个函数,numbers.Integral是int的虚拟超类
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple) #可以叠放多个register装饰器,让同一个函数支持不同类型
@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机制的一个显著特性,可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。
7.9 叠放装饰器
把@d1和@d2两个装饰器按顺序应用到f函数上,作用相当于f = d1(d2(f))。
7.10 参数化装饰器
#在clock装饰器的上,再添加一个功能:让用户传入一个格式字符串,控制被装饰函数的输出。
# clockdeco_param.py
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): #clock是参数化装饰器工厂函数
def decorate(func): #decorate是真正的装饰器
def clocked(*_args): #clocked包装被装饰的函数
t0 = time.time()
_result = func(*_args) #_result是被装饰的函数返回的真正结果
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) #_args是clocked的参数,args是用于显示的字符串
result = repr(_result) #result是_result的字符串表示形式,用于显示
print(fmt.format(**locals())) #这里使用**locals()是为了在fmt中引用clocked的局部变量
return _result #clocked会取代被装饰的函数,因此它应该返回被装饰的函数返回的值
return clocked #decorate返回clocked
return decorate #clock返回decorate
if __name__=='__main__':
@clock() #不传入参数调用clock(),因此应用的装饰器使用默认的格式str
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
运行结果:
[0.13200426s] snooze(0.123) -> None
[0.13200426s] snooze(0.123) -> None
[0.13100410s] snooze(0.123) -> None
受本书篇幅限制,我们队装饰器的探讨到此结束。装饰器最好通过实现__call__方法的类实现,不应该像本章的示例通过函数实现。这里是使用函数解说,更容易理解。
7.11 本章小结
***