当前位置:首页>python>从记忆化递归到类装饰器:理解 Python 装饰器的两种写法

从记忆化递归到类装饰器:理解 Python 装饰器的两种写法

  • 2026-09-02 20:02:33
从记忆化递归到类装饰器:理解 Python 装饰器的两种写法

很多人在学习 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 = {}

这里保存了两样东西:

  • self.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 = {}

用类来管理状态往往比闭包更容易维护。

装饰器的核心思想其实非常简单:

接收一个函数,对它进行包装,然后返回一个新的可调用对象来替代原函数。

这个“可调用对象”可以是:

  • 一个函数(函数装饰器)

  • 一个实现了 __call__ 的对象(类装饰器)

形式不同,但本质完全相同。

参考资料:
[1]《python程序设计与数据科学导论》作业
[2] chatgpt5.4

最新文章

随机文章