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 实际只被调用了几十次。

Read More: