Python decorator 被装饰函数的调用问题
装饰器模式(Decorator Pattern)可以在不需要改变函数实现的情况下,修改或者完善它的功能。多数情况下是在被装饰函数调用的之前和之后,添加逻辑从而实现所需要的功能,比如计时、日志、各种封装等等。那么这里会产生疑问,是否只能调用一次呢?不一定的。写一个 decorator,把被装饰函数调用多次,只要符合实际需求都是可以的。另外,这个被装饰函数是否一定要被调用到呢?实际上也没有这样的约束,这个可以参考标准库里提供的几个 decorator 来分析。
实际问题:
以 fibonacci 计算为例:
def fib(n: int) -> int:
if n in (0, 1):
return n
else:
return fib(n - 2) + fib(n - 1)
运行并测量时间:
>>> fib(35)
9227465
>>>
>>> import timeit
>>> timeit.timeit('fib(35)', number=1)
1.5118173000082606
可以看到,这种递归实现的效率很一般。如果统计一下,对于输入值 35,实际上 fib
被调用了几千万次。。
cache decorator
标准库 functools
模块里提供了包括 @functools.cache
、@functools.lru_cache
等 decorator。
这里暂时不考虑标准库里的实现,先用一个简单实现来说题。就如同 cache
这个名字所表示的,它的原理就是把计算输入及结果缓存起来,从而节约后续的计算。
def cache(func):
cache_dict = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
nonlocal cache_dict
# NOTE: 只是用于举例说明,所以没有用到 kwargs
if args not in cache_dict:
cache_dict[args] = func(*args, **kwargs)
return cache_dict[args]
return wrapper
@cache
def fib(n: int) -> int:
if n in (0, 1):
return n
else:
return fib(n - 2) + fib(n - 1)
再次运行并测量时间:
>>> fib(35)
9227465
>>>
>>> import timeit
>>> timeit.timeit('fib(35)', number=1)
6.499991286545992e-06
可以看到,时间开销几乎可以忽略不计,因为有了缓存,fib
实际只被调用了几十次。