装饰器和闭包(续)
实现一个简单的装饰器
定义一个装饰器,它在每次被修饰函数被调用时计时、然后把经过的时间、传入的参数、结果打印出来
1 import time 2 import functools 3 def clock(func): 4 def clocked(*args, **kwargs): 5 t0 = time.perf_counter() 6 result = func(*args, **kwargs) 7 elapsed = time.perf_counter() - t0 8 name = func.__name__ 9 arg_str = ", ".join(repr(arg) for arg in args) 10 kwarg_str = ", ".join("%s=%s"%(key, val) for key, val in kwargs.items()) 11 print("func : %s" % name) 12 print("args : %s" % arg_str) 13 print("kwargs : %s" % kwarg_str) 14 print("result : %s" % result) 15 print("elapsed: %s" % elapsed) 16 return result 17 return clocked 18 19 @clock 20 def snooze(sec): 21 time.sleep(sec) 22 23 @clock 24 def fact(n): 25 return n*fact(n-1) if n>1 else 1 26 27 @clock 28 def f(maxlen=10): 29 pass 30 snooze(3) 31 print(fact(10)) 32 f(maxlen=11)
需要说明的是,被clock修饰的函数,函数指向的都是clock内部的clocked函数。
@clock
def func(...):
pass
与func = clock(func)是等价的。
这就是装饰器常干的事情,把被装饰的函数替换成新的函数,两者接收相同的参数,返回相同的返回值,同时还会做些额外操作。
上面的装饰器clock还有点瑕疵,如果我们查看fact.__name__属性,会发现打印的是clocked,因为clock内部的clocked函数覆盖了原函数的__name__和__doc__属性。我们可以把原函数的相关属性复制到clocked中。functools.wraps就是做这个事情的。
1 import time 2 3 def clock(func): 4 @functools.wraps(func) 5 def clocked(*args, **kwargs): 6 t0 = time.perf_counter() 7 result = func(*args, **kwargs) 8 elapsed = time.perf_counter() - t0 9 name = func.__name__ 10 arg_str = ", ".join(repr(arg) for arg in args) 11 kwarg_str = ", ".join("%s=%s"%(key, val) for key, val in kwargs.items()) 12 print("func : %s" % name) 13 print("args : %s" % arg_str) 14 print("kwargs : %s" % kwarg_str) 15 print("result : %s" % result) 16 print("elapsed: %s" % elapsed) 17 return result 18 return clocked 19 20 @clock 21 def snooze(sec): 22 time.sleep(sec) 23 24 @clock 25 def fact(n): 26 return n*fact(n-1) if n>1 else 1 27 28 @clock 29 def f(maxlen=10): 30 pass 31 snooze(3) 32 print(fact(10)) 33 f(maxlen=11)
标准库中的装饰器
python内置了三个用来装饰方法的函数:property、calssmethod、staticmethod,我们在后面讨论。
另一个常见的装饰器是functools.wraps,他可以协助创建行为良好的装饰器函数,我们已经用过。除此之外,还有两个装饰器值得我们关注下.
lru_cache和singledispatch,这两个装饰器都在functools模块中。
functools.lru_cache
functools.lru_cache是非常实用的装饰器,他实现了备忘录的功能(缓存机制)。他对函数进行优化,把运行结构保存下来,避免下次传入参数时进行重复计算。lru是“Least-Recently-Used”缩写,缓存不是无限制的增长,达到设定的最大之后,多余的会被扔掉。
斐波那契数列的递归生成方式,非常耗时,他很适合实用lru_cache。
不使用lru_cache装饰器
1 import time 2 import functools 3 def clock(func): 4 @functools.wraps(func) 5 def clocked(*args, **kwargs): 6 t0 = time.perf_counter() 7 result = func(*args, **kwargs) 8 elapsed = time.perf_counter() - t0 9 name = func.__name__ 10 arg_str = ", ".join(repr(arg) for arg in args) 11 kwarg_str = ", ".join("%s=%s"%(key, val) for key, val in kwargs.items()) 12 #print("func : %s" % name) 13 #print("args : %s" % arg_str) 14 #print("kwargs : %s" % kwarg_str) 15 #print("result : %s" % result) 16 #print("elapsed: %s" % elapsed) 17 print("[%0.8f] %s(%s) => %s" %(elapsed, name, arg_str, result)) 18 return result 19 return clocked 20 21 @clock 22 def fibonacci(n): 23 return fibonacci(n-2)+fibonacci(n-1) if n > 2 else 1 24 25 print(fibonacci(6))
运行结果如下:
[0.00000066] fibonacci(2) => 1
[0.00000033] fibonacci(1) => 1
[0.00000033] fibonacci(2) => 1
[0.00004171] fibonacci(3) => 2
[0.00007846] fibonacci(4) => 3
[0.00000033] fibonacci(1) => 1
[0.00000033] fibonacci(2) => 1
[0.00002715] fibonacci(3) => 2
[0.00000033] fibonacci(2) => 1
[0.00000033] fibonacci(1) => 1
[0.00000033] fibonacci(2) => 1
[0.00002549] fibonacci(3) => 2
[0.00005032] fibonacci(4) => 3
[0.00011587] fibonacci(5) => 5
[0.00023373] fibonacci(6) => 8
8
从结果可以看出,在计算fibonacci(6)有着大量的重复计算,如果计算fibonacci(100),那耗时将相当长。
使用lru_cache可以保存结果供之后使用,从而提高效率,lru_cache装饰器有两个关键字参数。
maxsize:缓存大小,默认值128,为了使效率更高,最后设置成2的n次幂
typed:默认为False,如果设置成True,表示不同的类型会被分开缓存,如f(3)和f(3.0)会被当成两个不同的类型分别缓存。
1 def clock(func): 2 @functools.wraps(func) 3 def clocked(*args, **kwargs): 4 t0 = time.perf_counter() 5 result = func(*args, **kwargs) 6 elapsed = time.perf_counter() - t0 7 name = func.__name__ 8 arg_str = ", ".join(repr(arg) for arg in args) 9 kwarg_str = ", ".join("%s=%s"%(key, val) for key, val in kwargs.items()) 10 #print("func : %s" % name) 11 #print("args : %s" % arg_str) 12 #print("kwargs : %s" % kwarg_str) 13 #print("result : %s" % result) 14 #print("elapsed: %s" % elapsed) 15 print("[%0.8f] %s(%s) => %s" %(elapsed, name, arg_str, result)) 16 return result 17 return clocked 18 19 @functools.lru_cache(maxsize=128) 20 @clock 21 def fibonacci(n): 22 return fibonacci(n-2)+fibonacci(n-1) if n > 2 else 1 23 24 print(fibonacci(6))
运行结果如下:
[0.00000033] fibonacci(2) => 1
[0.00000066] fibonacci(1) => 1
[0.00002152] fibonacci(3) => 2
[0.00008310] fibonacci(4) => 3
[0.00000132] fibonacci(5) => 5
[0.00011852] fibonacci(6) => 8
8
运行时间大大缩小,没有了多余的重复计算过程,所以即使计算fibonacci(100)也是很快的。
顺便一提,lru_cache使用字典存储结果,键根据调用时传入的参数创建,因此被lru_cache修饰的函数的各个参数都必须是可散列对象。
functools.singledispatch
python可以直接打印列表、元组、字典等。如果我们想要自己定义打印形式呢?
python的functools.singledispatch装饰器可以把整体方案拆分成多个模块,甚至可以为逆无法修改的类提供专门的函数。使用@functools.singledispatch装饰的函数会变成一个泛函数:泛函数根据第一个参数的类型,以不同的方式执行相同操作的一组函数。
import numbers import functools class Foo: def __init__(self): self.data = "foo object" #处理的基函数 @functools.singledispatch def show(obj): print(obj) #专门处理各个类型的函数用@show.register(type)修饰,函数名无所谓 @show.register(numbers.Integral) def _(n): print("numbers: %d" % n) @show.register(str) def _(s): print("str: %s" % s) @show.register(dict) def _(d): s = ["key: %s ==> val: %s"%(key, val) for key, val in d.items()] print("dict: %s" % s) @show.register(Foo) def _(f): print("<class Foo> object, data: %s" % f.data) show(1) show("python") show({"one":1, "two":2, "thee":3}) show(Foo()) show([1,2,3])
1、show函数用@functools.singledispatch修饰
2、使用@show.register(type)来注册各个处理不同类型的具体函数
3、专门函数的名称无关紧要
4、可以同时注册多个register装饰器,使得同一个函数可以处理多个类型
import array
@show.register(list)
@show.register(array.array)
def _(arr):
...
singledispatch的一个显著特征是你可以在系统的任何地方和任何模块中注册函数。如果后来在新的模块中定义了新的类型,那么可以轻松添加针对新类型的专门函数而不必担心其影响。
叠放装饰器
上面用lru_cache装饰fibonacci数列的例子,已经演示了如何叠放装饰器。
把@d1和@d2两个装饰器应用到f函数上,等价于f = d1(d2(f))。
也就是
@d1 @d2 def f(): pass
与
f = d1(d2(f))
是一样的。
参数化装饰器
观察上面由@functools.singledispatch生成的泛函数show,我们在注册的时候使用了@show.register(type)形式,也就是说show.register装饰器带一个参数。如何才能让装饰器参数化呢?答案就是闭包,闭包可以为函数绑定自由变量,参数化装饰器就是闭包的应用之一。
现有如下的装饰器,将被他修饰的函数放到registry列表中。
1 registry = [] 2 3 def register(func): 4 registry.append(func) 5 return func 6 7 @register 8 def f1(): 9 print("running f1()") 10 11 print("registry ->", registry) 12 f1()
参数化注册装饰器
现在我们想通过参数控制注册功能,如果可选参数active为True,执行注册功能,为False,不注册函数。
1 registry = set() 2 3 def register(active=True): 4 def deco(func): 5 print("running register(active=%s) -> %s" %(active, func)) 6 if active: 7 registry.add(func) 8 else: 9 registry.discard(func) 10 return func 11 return deco 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 f1() 25 f2() 26 f3() 27 print(registry)
只有f2在registry中,因为f1被@register(active=False)修饰,所以f1并没有被添加到registry中。
参数化装饰器的原理相当复杂,我们刚刚讨论的那个比大多数都要简单,参数化装饰器通常都会把被装饰的函数替换掉,而且结构上多一层嵌套。
by the way:Graham Dumpleton和Lennart Regebro都认为,装饰器最好通过实现类的__call__方法实现,不应当通过函数。
posted on 2019-03-05 13:38 forwardFields 阅读(243) 评论(0) 编辑 收藏 举报