Python装饰器
装饰器
Python中装饰器的本质实际上是一个函数,在一些场景中,我们可能面临这切面编程的需求,例如:计算一个函数执行的时间,于是我们需要在函数执行之前和执行之后捕捉时间,再把两个时间相减,得到函数的执行时间,又比如在执行某些函数的时候,需要在执行之前在日志上打印开始执行,执行完毕后再打印执行完毕,而我们又不想改变我们的业务代码,而Python中的装饰器,则为我们提供了便利。
假如,有一个名为f1的装饰器,来打印函数的开始执行和执行完毕,我们便可以把函数当成一个参数传入f1函数中,再调用func()执行
>>> def f1(func): ... print("%s start" % func.__name__) ... func() ... print("%s end" % func.__name__) ... >>> def foo(): ... print("foo") ... >>> f1(foo) foo start foo foo end
而且,我们可以把f1函数当成注解配置在foo上,但这样会引来另一个问题,就是我们没有调用foo,但foo会传入f1执行一遍
>>> def f1(func): ... print("func start") ... func() ... print("func end") ... >>> @f1 ... def foo(): ... print("foo") ... func start foo func end
于是,我们稍微修改一下代码
>>> def f1(func): ... def wapper(*args, **kwargs): ... """wapper doc""" ... print("%s start" % func.__name__) ... res = func(*args, **kwargs) ... print("%s start" % func.__name__) ... return res ... return wapper ... >>> @f1 ... def foo(): ... """foo doc""" ... print("foo") ... >>> foo.__name__ 'wapper' >>> foo.__doc__ 'wapper doc'
这样,就不会出现之前未调用foo,但是foo函数自动执行的事件了,而且用注解,会相比于之前用f1(foo)的方式美观的多,也不会破坏代码结构。但是,当我们打印foo的函数名称和注释时
>>> foo.__name__ 'wapper' >>> foo.__doc__ 'wapper doc'
我们会发现,打印出来的并不是foo和foo doc,而是wapper和他本身的注释,所以,我们可以用functools模块的wraps来解决
>>> from functools import wraps >>> def f1(func): ... @wraps(func) ... def wapper(*args, **kwargs): ... """wapper doc""" ... print("%s start" % func.__name__) ... res = func(*args, **kwargs) ... print("%s start" % func.__name__) ... return res ... return wapper ... >>> @f1 ... def foo(): ... """foo doc""" ... print("foo") ... >>> foo() foo start foo foo start >>> foo.__name__ 'foo' >>> foo.__doc__ 'foo doc'
使用functools.lru_cache来缓存函数执行结果
functools.lru_cache是非常实用的装饰器,它可以缓存函数执行的结果,从而避免传入相同函数时重复计算
>>> from functools import lru_cache >>> @lru_cache() ... def add(x, y): ... res = x + y ... print("%s+%s=%s" % (x, y, res)) ... return res ... >>> add(1, 2) 1+2=3 3 >>> add(1, 2) 3 >>> add(2, 3) 2+3=5 5
从上面可以看到,add函数每次会打印传入的参数和结果,当我们第一次传入add(1, 2),控制台打印了,但是传入第二次的时候,控制台只打印了3,但是当我们替换传入参数时,控制台又打印了。注意:functools.lru_cache适合用来做一些类似1+2=3或者2+3=5这样恒等的事,如果有声明全局变量并在函数里或加或减并对函数执行的结果有影响,那么这个注解就不适用了。下面,让我们打印第n个斐波那契数,看看用不用lru_cache会有什么差别
我们定义一个计时函数,用来计算函数执行的时间:
def clock(func): def clocked(*args, **kwargs): t0 = time.perf_counter() res = 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, res)) return res
然后打印第6个斐波那契数
>>> @clock ... def fibonacci(n): ... if n < 2: ... return n ... return fibonacci(n - 2) + fibonacci(n - 1) ... >>> fibonacci(6) [0.00000164s] fibonacci(0) -> 0 [0.00000164s] fibonacci(1) -> 1 [0.00279254s] fibonacci(2) -> 1 [0.00000082s] fibonacci(1) -> 1 [0.00000123s] fibonacci(0) -> 0 [0.00000205s] fibonacci(1) -> 1 [0.00299864s] fibonacci(2) -> 1 [0.00540526s] fibonacci(3) -> 2 [0.01198674s] fibonacci(4) -> 3 [0.00000082s] fibonacci(1) -> 1 [0.00000082s] fibonacci(0) -> 0 [0.00000205s] fibonacci(1) -> 1 [0.00224980s] fibonacci(2) -> 1 [0.00452218s] fibonacci(3) -> 2 [0.00000082s] fibonacci(0) -> 0 [0.00000123s] fibonacci(1) -> 1 [0.00218616s] fibonacci(2) -> 1 [0.00000082s] fibonacci(1) -> 1 [0.00000082s] fibonacci(0) -> 0 [0.00000246s] fibonacci(1) -> 1 [0.00242469s] fibonacci(2) -> 1 [0.00452628s] fibonacci(3) -> 2 [0.00964129s] fibonacci(4) -> 3 [0.01636810s] fibonacci(5) -> 5 [0.03175828s] fibonacci(6) -> 8
现在我们尝试用lru_cache缓存,看看执行效率会不会提高
>>> @lru_cache() ... @clock ... def fibonacci(n): ... if n < 2: ... return n ... return fibonacci(n - 2) + fibonacci(n - 1) ... >>> fibonacci(6) [0.00000123s] fibonacci(0) -> 0 [0.00000287s] fibonacci(1) -> 1 [0.00444294s] fibonacci(2) -> 1 [0.00000369s] fibonacci(3) -> 2 [0.00705895s] fibonacci(4) -> 3 [0.00000287s] fibonacci(5) -> 5 [0.01016884s] fibonacci(6) -> 8
对比两次执行,可以看到第一次的第六个斐波那契数需要0.031秒左右的时间,而第二个则只需要0.01秒,性能有明显的提高,当然只执行一次并不能说明什么,可以使用timeit模块执行多次,看看二者的性能,这里就不再比较。