装饰器和闭包(续)

实现一个简单的装饰器

定义一个装饰器,它在每次被修饰函数被调用时计时、然后把经过的时间、传入的参数、结果打印出来

 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编辑  收藏  举报

导航