很多人在学习 Python 装饰器时,最常见的写法是这样的:
defdecorator(func):defwrapper(*args, **kwargs): ...return func(*args, **kwargs)return wrapper
久而久之,很多人会形成一种印象:装饰器就是一个函数。但实际上,这只是最常见的一种写法,并不是唯一形式。
来看一个稍微不同的例子:
classMemoize:def__init__(self, func): self.func = func self.memo = {}def__call__(self, x):if x notin self.memo: self.memo[x] = self.func(x)return self.memo[x]@Memoizedeffib(n):return n if n < 2else fib(n-1) + fib(n-2)print(fib(5))
程序最终输出:
5
如果你第一次看到这种写法,可能会产生一个疑问:为什么装饰器变成了一个类?
要理解这个问题,我们需要先弄清楚装饰器到底在做什么。
装饰器的本质:替换函数
当 Python 解释器看到下面的代码时:
@Memoizedeffib(n):return n if n < 2else fib(n-1) + fib(n-2)
它实际上会把代码转换成下面这种形式:
deffib(n):return n if n < 2else fib(n-1) + fib(n-2)fib = Memoize(fib)
也就是说,函数 fib 在定义完成后,并不会直接保留,而是会被传入 Memoize。Memoize(fib) 返回的结果会重新赋值给 fib。
所以此时:
fib
已经不再是原来的函数,而是 Memoize 类的一个实例对象。
这也是为什么装饰器既可以是函数,也可以是类。只要它能接收一个函数,并返回一个“可调用对象”,就可以充当装饰器。
为什么这个类可以像函数一样调用
在 Python 中,只要一个对象实现了 __call__ 方法,它就可以像函数一样被调用。
在 Memoize 类中:
def__call__(self, x):
意味着这个对象可以这样使用:
fib(5)
实际上执行的是:
fib.__call__(5)
也就是说,虽然 fib 看起来像一个函数,但其实它已经变成了一个对象,而这个对象恰好实现了调用行为。
Memoize 做了什么事情
Memoize 这个装饰器的作用,是给函数增加一个非常常见的优化:缓存(memoization)。
看一下初始化方法:
def__init__(self, func): self.func = func self.memo = {}
这里保存了两样东西:
当函数被调用时:
def__call__(self, x):if x notin self.memo: self.memo[x] = self.func(x)return self.memo[x]
逻辑非常简单:
如果这个参数以前没有计算过,就调用原始函数计算一次,并把结果存起来;如果已经计算过,就直接从缓存中返回。
这就是所谓的 记忆化(memoization)。
为什么斐波那契特别适合这个装饰器
普通递归实现的斐波那契函数存在大量重复计算。
例如:
fib(5)= fib(4) + fib(3)= (fib(3) + fib(2)) + (fib(2) + fib(1))
你会发现 fib(3) 和 fib(2) 会被重复计算很多次。
而 Memoize 会把每次计算结果存入 memo:
memo = {0: 0,1: 1,2: 1,3: 2,4: 3,5: 5}
当再次需要某个值时,就不再递归,而是直接读取缓存。
因此原本指数级复杂度的算法,变成了线性复杂度。
为什么递归还能正常工作
还有一个非常关键的细节。
在函数体中我们写的是:
fib(n-1) + fib(n-2)
但此时 fib 已经不是原函数,而是 Memoize 的实例。
所以递归调用时,其实执行的是:
__call__(n-1)
也就是说,每一次递归都会自动经过缓存检查。这就是装饰器在递归函数中非常强大的原因。
用函数装饰器实现同样的功能
其实同样的功能也可以用函数装饰器实现:
defmemoize(func): memo = {}defwrapper(x):if x notin memo: memo[x] = func(x)return memo[x]return wrapper@memoizedeffib(n):return n if n < 2else fib(n-1) + fib(n-2)
这个版本的核心逻辑完全一样,只不过缓存字典 memo 被保存在函数闭包里,而不是对象属性里。
函数装饰器与类装饰器的区别
两种写法其实表达的是同一件事:接收原函数,然后返回一个新的可调用对象。
函数装饰器返回的是一个新的函数:
func → wrapper
类装饰器返回的是一个对象:
func → Memoize实例
但只要这个对象实现了 __call__,它就可以像函数一样被调用。
在很多情况下,函数装饰器已经足够使用。但当装饰器需要保存比较复杂的状态时,类装饰器会更加清晰。
例如本例中的缓存:
self.memo = {}
用类来管理状态往往比闭包更容易维护。
装饰器的核心思想其实非常简单:
接收一个函数,对它进行包装,然后返回一个新的可调用对象来替代原函数。
这个“可调用对象”可以是:
形式不同,但本质完全相同。