上一节我们讲到了装饰器的基础知识,并且讲到了functools.wraps内置装饰器,由于接下来的内容比较复杂,所以分开进行说明。好了,让我们更深入挖掘装饰器吧!
4、标准库中的装饰器
常见的装饰器是functools.wraps,它的作用是协助构建行为良好的装饰器,我们已经说过了,剩余标准库中最值得关注的两个装饰器是lru_cache和全新的singledispatch,赶紧来看看吧。
4.1 使用functools.lru_cache做备忘
functools.lru_cache是非常实用的装饰器,它实现了备忘(memoization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。我们以生成斐波那契数列为例:
import time
import functools
# 装饰器函数
def clock(func):
name = func.__name__
@functools.wraps(func)
def clocked(*arg,**kwargs):
n = 1
if arg:
n = int(arg[0])
start = time.perf_counter()
result = func(n)
cost_time = time.perf_counter() - start
print("[{0:.8f}s] {1:s}({2:d})->{3:d}".format(cost_time, name, n,
result))
return result
return clocked
# 装饰器定义
@clock
def fb(n):
'''生成斐波那契数列'''
return 1 if n < 3 else fb(n - 1) + fb(n - 2)
>>>fb(6)
# 返回
[0.00000040s] fb(2)->1
[0.00000030s] fb(1)->1
[0.00007020s] fb(3)->2
[0.00000030s] fb(2)->1
[0.00033980s] fb(4)->3
[0.00000030s] fb(2)->1
[0.00000020s] fb(1)->1
[0.00003310s] fb(3)->2
[0.00042010s] fb(5)->5
[0.00000030s] fb(2)->1
[0.00000020s] fb(1)->1
[0.00003300s] fb(3)->2
[0.00000030s] fb(2)->1
[0.00009420s] fb(4)->3
[0.00055190s] fb(6)->8
8
看到了没,采用这种慢速递归函数尽管只计算到fb(8),但是出现了大量的重复运算,效率低下。现在我们用lru_cache来实现一下看看有什么变化:
@functools.lru_cache() # 注意调用方式
@clock
def fb(n):
return 1 if n < 3 else fb(n - 1) + fb(n - 2)
>>>fb(6)
# 返回
[0.00000040s] fb(2)->1
[0.00000050s] fb(1)->1
[0.00023260s] fb(3)->2
[0.00027590s] fb(4)->3
[0.00030300s] fb(5)->5
[0.00032790s] fb(6)->8
8
效果是立竿见影的。除此之外,lru_cache还有两个可选参数:
functools.lru_cache(maxsize=128, typed=False)
- maxsize参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize应该设为2的幂。
- typed参数如果设为True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如1和1.0)区分开。
- 顺便说一下,因为lru_cache使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的。
4.2 单分派泛函数
我们以一个简单的例子来说明,比如说我们输入一个对象,想把它直接打印出来,但是对于字符串或者整数,需要加上汉字的前缀,你准备怎么办?我一开始就想,直接用if/elif判断一下输入类型不就好了,确实是一个思路,但是这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数(if)会变得很大,而且每一个if都必须构建一个专门函数。
这时候functools.singledispatch
就是一个不错的选择,作为装饰器来使用可以把整体方案拆分成多个小的模块,甚至可以为你无法修改的类提供专门的函数。使用 @singledispatch 装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。
from functools import singledispatch
import numbers
# 用@singledispatch 标记基函数
@singledispatch
def print_type(obj):
print(repr(obj))
# 各个专门函数使用@«base_function».register(«type»)装饰。
@print_type.register(str)
def _(text): # 专门函数的名称无关紧要;_是个不错的选择,简单明了
print('字符串:', text)
# 可以叠放多个register装饰器,让同一个函数支持不同类型。
@print_type.register(numbers.Integral)
def _(n):
print('整数:', n)
>>>print_type(1)
整数: 1
>>>print_type("我祝你们开心")
字符串: 我祝你们开心
# 未标注的类型仍按最初的repr(obj)打印
>>>print_type(abs)
<built-in function abs>
只要可能,注册的专门函数应该处理抽象基类(如numbers.Integral和abc.MutableSequence),不要处理具体实现(如int和list)。这样,代码支持的兼容类型更广泛。
5、叠放装饰器
叠放装饰器就是在定义的时候使用多个装饰函数,前面我们已经见过了,在4.1中我们使用lru_cache以及clock两个装饰函数对fb进行装饰。
def d1(func):
print('a')
return func
def d2(func):
print('b')
return func
@d1
@d2
def f():
print('c')
# 装饰器定义的时候就被执行,没忘吧
a
b
事实上等价于:f=d1(d2(f))
6、参数化装饰器
前面我们已经学过,Python把被装饰的函数作为第一个参数传给装饰器函数,而被装饰函数的参数在装饰器函数内构造一个内部函数进行接收。如果没有印象,可以参考上一篇文章装饰器(上)中的实现一个简单的装饰器函数。今天我们讨论一个更复杂的问题:装饰器本身如果也要独立外部参数呢?应该传递给谁?
我们以一个简单的例子进行说明,被装饰函数对于被一个进门的警官,都会说一声Hello,装饰器会说出Yes,但是会需要一个额外的性别参数确定确定前缀是MR还是MS:
def factory(sex='man'): # 装饰器工厂函数,接受装饰器所需独立参数
def decorat(func): # 真实的装饰器
def yes(name): # 内部函数,接收被装饰函数的参数
func(name)
if sex == 'man':
print('Yes, Mr.', name)
else:
print('Yes, Ms.', name)
return yes
return decorat
# 带参数的装饰器定义
@factory('woman')
def come(name='None'):
print(name, ' is comming!')
我们一定要区分的参数的不同:
- 一个是装饰器的参数,这个参数在你定义@的时候就进行传参,传递给装饰器工厂函数factory。调用它会返回真正的装饰器decorat,这才是应用到目标函数上的装饰器。
- 一个是被装饰函数的参数,通过在装饰器内定义一个内部函数进行传参,比如yes内部函数
如果你不想使用@语法,要像常规函数那样传递上面的两个参数,那么装饰器应该定义为:
def come(name='None'):
print(name, ' is comming!')
come=factory(sex='woman')(come)
感觉这样的三层嵌套很复杂是吧,好吧,我也觉得很复杂,所以装饰器到这里就结束了吧,不再深入地讨论了。
若想真正理解装饰器,需要区分导入时和运行时,还要知道变量作用域、闭包和新增的nonlocal声明。掌握闭包和nonlocal不仅对构建装饰器有帮助,还能协助你在构建GUI程序时面向事件编程,或者使用回调处理异步I/O。
另外最后说一点,不是很推荐使用函数作为装饰器,尽管我们为了便于讲述是这么用的,但是事实上更推荐按最好通过__call__方法的类实现。两者的区别不是很大,因为前面我们讲过的,任何实现了__call__方法的对象其行为模式都表现地像个函数。比如上面的例子,使用类实现如下所示:
class factory():
# 装饰器独立参数通过__init__传递
def __init__(self, sex='man'):
self.sex = sex
# 真实的装饰器
def __call__(self, func):
# 被装饰函数的参数同样需要构造内部函数进行传递
def yes(name):
func(name)
if self.sex == 'man':
print('Yes, Mr.', name)
else:
print('Yes, Ms.', name)
return yes
# 以下语法一样
@factory(sex='man')
def come(name='None'):
print(name, ' is comming!')
——本章完——
欢迎关注我的微信公众号