第九章 装饰器与闭包
第九章 装饰器与闭包
定义装饰器
装饰器是一个可调用对象(callable),接收另一个函数作为参数(即被装饰的函数)。装饰器可以对被装饰函数进行处理;返回原函数,或用另一个函数/可调用对象替换它。
装饰器本质
装饰器本质上是语法糖。在元编程(metaprogramming)等场景中,直接调用装饰器并传入函数有时更为灵活。使用 @decorator
语法糖等价于显式调用装饰器并重新赋值。
# 装饰器语法
@decorate
def target():print('running target()')
# 脚本式写法
def target():print('running target()')target = decorate(target)
两种写法效果完全相同:执行后,target
名称绑定到 decorate(target)
的返回值,该返回值可能是原函数,也可能是另一个函数。
装饰器替换函数
以下脚本演示了装饰器如何用内部函数替换原函数:
# 装饰器使用内部函数替换原函数
def deco(func):def inner():print('running inner()')return inner@deco
# 调用 target() 实际执行的是 inner 函数;
def target(): print('running target()')target()
# target 名称现在引用的是 deco 内部定义的 inner 函数。
print(target)
# 输出
# running inner()
# <function deco.<locals>.inner at 0x...>
装饰器核心特性
- 装饰器本身是一个可调用对象(通常是函数);
- 可能替换被装饰的函数;
- 在模块加载时立即执行(即装饰发生在导入时,而非调用时)。
何时执行装饰器
装饰器在被装饰函数定义之后立即执行。该过程发生在 模块加载时(import time),即 Python 导入模块的过程中。被装饰的函数本身不会立即执行,只有在被显式调用时才运行。这体现了 Python 中“导入时”(import time)与“运行时”(runtime)的关键区别。此机制常用于函数注册、插件系统、缓存初始化等场景。
registry = [] # 保存被 @register 装饰的函数引用def register(func):print(f'running register({func})') # 显示正在装饰的函数registry.append(func) # 将函数加入注册列表return func # 返回原函数(未替换)@register
def f1():print('running f1()')@register
def f2():print('running f2()')def f3(): # 未被装饰print('running f3()')def main():print('running main()')print('registry ->', registry)f1()f2()f3()if __name__ == '__main__':main()
# 作为脚本运行时
$ python3 registration.py
# register 在 main() 执行前已调用两次
# 装饰器接收到的是函数对象(如 <function f1 at 0x...>)
running register(<function f1 at 0x...>)
running register(<function f2 at 0x...>)
running main()
# registry 在模块加载完成后已包含 f1 和 f2 的引用
registry -> [<function f1 at 0x...>, <function f2 at 0x...>]
# 函数体(如 print('running f1()'))仅在 main() 中调用时才执行
running f1()
running f2()
running f3()
# 仅导入模块时
>>> import registration
# 即使不运行 main(),装饰器仍会在导入时执行
running register(<function f1 at 0x...>)
running register(<function f2 at 0x...>)
>>> registration.registry
# registry 已被填充,说明装饰器逻辑在导入阶段完成
[<function f1 at 0x...>, <function f2 at 0x...>]
@register
装饰器在两个方面与实际用法有所不同
-
定义位置:
装饰器函数(register
)与被装饰函数(f1
,f2
)定义在同一模块中。
→ 实际项目中,装饰器通常在一个模块中定义,然后在其他模块中使用。 -
行为方式:
register
装饰器直接返回原函数,未做任何修改。
→ 大多数真实装饰器会定义一个内部函数(inner function)并返回它,从而替换原函数。
虽然 register
没有修改被装饰函数,但“原样返回”是一种有效模式。广泛应用于框架中,用于将函数注册到中心注册表(registry)。典型场景如将 URL 路径映射到处理函数(如 Web 框架中的路由装饰器)。这类装饰器可能修改也可能不修改原函数,核心目的是注册。
变量作用域
Python 函数在访问变量时遵循以下规则:
- 局部变量:在函数参数或函数体内被赋值的变量。
- 全局变量:在模块顶层(函数/类外部)定义的变量。
# 读取局部与全局变量
def f1(a):print(a)print(b) # b 未在函数内赋值 → 视为全局变量
# 若全局未定义 b,调用 f1(3) 会抛出 NameError。
赋值决定作用域
只要在函数体内对某个变量赋值,Python 在编译时就将其视为局部变量,无论赋值语句出现在何处。
# 在外部进行了定义
# 但是变量 b 被视为局部变量,因为它在函数体内被赋值
b = 6
def f2(a):print(a)print(b) # ← 报错# b = 9
虽然 print(b)
在 b = 9
之前,但因函数内存在对 b
的赋值,整个函数体中 b
都被视为局部变量。调用 f2(3)
时,print(a)
成功(输出 3
),但 print(b)
尝试读取尚未赋值的局部变量 b
,导致报错。
UnboundLocalError: local variable 'b' referenced before assignment
这不是 bug,而是 Python 的设计:无需显式声明变量,但赋值即定义为局部。相比 JavaScript(未声明变量会污染全局),此行为更安全。
显式声明全局变量global
若需在函数内修改全局变量,必须使用 global
声明:
b = 6
def f3(a):# 显示声明global bprint(a)print(b)b = 9
# 输出 3 6
# 之后全局变量 b 变为9
- 局部作用域(Local)
函数参数及函数体内赋值的变量。 - 全局作用域(Global / Module-level)
模块顶层定义的名称。 - 非局部作用域(Nonlocal)
由嵌套函数中的闭包引入。
作用域判定
通过反汇编可验证作用域判定发生在编译阶段:
# f1的字节码 此时b为全局
dis(f1)
# ...
LOAD_GLOBAL 1 (b) # ← 从全局加载 b
# f2的字节吗 此时b被视为局部
dis(f2)
# ...
LOAD_FAST 1 (b) # ← 试图从局部加载 b(但尚未赋值)
STORE_FAST 1 (b) # 赋值发生在之后
LOAD_FAST
表示访问局部变量;LOAD_GLOBAL
表示访问全局变量;编译器在函数定义时就已根据是否存在赋值决定使用哪种指令。此机制说明:变量作用域由静态分析(编译时)决定,而非运行时动态查找。
闭包
闭包是一个函数(记为 f
),它拥有一个扩展的作用域,该作用域包含在 f
的函数体中引用、但既非全局变量也非 f
的局部变量的变量。这些变量必须来自包含 f
的外层函数的局部作用域。函数是否匿名(如 lambda)无关紧要;关键在于能否访问其外部非全局变量。**闭包 ≠ 匿名函数。**混淆源于两者常在嵌套函数场景中同时出现。
实现方式
# 面向对象表示
class Averager():def __init__(self):# 历史数据保存在实例属性 self.series 中。self.series = []def __call__(self, new_value):self.series.append(new_value)total = sum(self.series)return total / len(self.series)# 使用方式
avg = Averager()
print(avg(10)) # 10.0
print(avg(11)) # 10.5
print(avg(12)) # 11.0
# 函数式实现
def make_averager():series = [] # 外层函数的局部变量def averager(new_value):series.append(new_value) # 引用外层变量total = sum(series)return total / len(series)return averager# 使用方式
avg = make_averager()
print(avg(10)) # 10.0
print(avg(11)) # 10.5
print(avg(15)) # 12.0
averager
是一个闭包,它“记住”了外层函数 make_averager
中的 series
列表。即使 make_averager()
已返回、其局部作用域已销毁,series
仍可通过闭包访问。
自由变量
在 averager
中,series
是一个自由变量(free variable),它在 averager
的局部作用域中未被绑定(即未赋值),但被引用。
自由变量的绑定
Python 通过以下机制保存自由变量的绑定:
# 函数对象的元数据
print(avg.__code__.co_varnames) # ('new_value', 'total')
print(avg.__code__.co_freevars) # ('series',)
co_varnames
:局部变量名;co_freevars
:自由变量名。
# 闭包的实际存储 __closure__
print(avg.__closure__) # (<cell at 0x...: list object at 0x...>,)
print(avg.__closure__[0].cell_contents) # [10, 11, 15]
__closure__
是一个元组,每个元素对应一个自由变量;每个元素是 cell 对象,其 cell_contents
属性保存变量的实际值。
总的来说,闭包保留了定义时自由变量的绑定,即使外层函数已退出。必要条件是存在嵌套函数;内层函数引用了外层函数的局部变量;外层函数返回内层函数(或以其他方式使其在外部可调用)。闭包使得函数式编程中实现状态保持(如累加器、计数器、缓存等)成为可能,而无需依赖类或全局变量。
nonlocal
声明
背景
在闭包中,若尝试对不可变类型(如 int
、str
、tuple
)的自由变量进行赋值(如 count += 1
),会触发 UnboundLocalError
。
# 有缺陷的实现
def make_averager():count = 0total = 0def averager(new_value):count += 1 # 等价于 count = count + 1total += new_value # 同样是重新赋值return total / countreturn averager
[!NOTE]
Python 在编译时判定
count
和total
为averager
的局部变量只要在函数体内对某个变量进行了赋值(无论位置在何处),该变量在整个函数作用域中都被视为局部变量。
count += 1
实际是 count = count + 1
,属于赋值操作;Python 在编译时判定 count
和 total
为 averager
的局部变量;但调用时局部变量尚未初始化,导致:
UnboundLocalError: local variable 'count' referenced before assignment
# 函数式实现 def make_averager():series = [] # 外层函数的局部变量def averager(new_value):series.append(new_value) # 引用外层变量total = sum(series)return total / len(series)return averager# 使用方式 avg = make_averager() print(avg(10)) # 10.0 print(avg(11)) # 10.5 print(avg(15)) # 12.0
对比:上述代码中使用
series.append()
是就地修改可变对象,是调用方法修改可变对象的内容,并未对series
这个名字进行重新绑定,因此series
仍是自由变量,可被闭包捕获。
[!IMPORTANT]
可变对象(mutable objects)
- 特点:对象创建后,其内容可以被修改,而对象的身份(id)保持不变。
- 修改方式:通过调用对象的原地修改方法(如
list.append()
、dict.update()
、set.add()
等)。- 关键点:这些操作不改变变量名的绑定,只是修改了对象内部状态。
lst = [1, 2] print(id(lst)) # 例如 140234... lst.append(3) # 修改内容,未重新赋值 print(id(lst)) # 仍是 140234...(同一对象)
不可变对象(immutable objects)
- 特点:对象一旦创建,其内容不能被修改。
- “修改”方式:只能通过创建新对象,并将变量名重新绑定到新对象。
- 关键点:任何看似“修改”的操作(如
x += 1
、s = s + "a"
)实际上都是创建新对象 + 重新赋值。x = 10 print(id(x)) # 例如 945... x += 1 # 等价于 x = x + 1 → 创建新 int 对象 print(id(x)) # 变为另一个值(新对象)
解决方案
Python 3 引入 nonlocal
关键字,用于显式声明变量来自外层函数作用域,即使在内层函数中对其赋值。
def make_averager():count = 0total = 0def averager(new_value):nonlocal count, total # 声明为非局部变量count += 1total += new_valuereturn total / countreturn averager
count
和 total
被视为外层函数 make_averager
的局部变量;赋值操作会更新闭包中存储的绑定;使代码可正常工作。
avg = make_averager()
print(avg(10)) # 10.0
print(avg(11)) # 10.5
print(avg(12)) # 11.0
Python 变量查找逻辑
当函数中引用变量 x
时,Python 按以下规则确定其作用域:
-
global x
声明
→x
来自并赋值于模块全局作用域。 -
nonlocal x
声明
→x
来自并赋值于最近的外层函数局部作用域(即闭包中的自由变量)。 -
x
是参数或在函数体内被赋值
→x
是局部变量。 -
x
仅被引用(未赋值、非参数)
查找顺序为:- 外层函数的局部作用域(逐层向外,即非局部作用域);
- 模块全局作用域;
- 内置命名空间(
__builtins__.__dict__
)。
注意:Python 没有“程序级”全局作用域,只有模块级全局作用域。
实现简单装饰器
简易版本
import timedef clock(func):# *args接受任意位置参数 但是无法处理关键字参数def clocked(*args): # 记录函数调用开始时间(高精度计时器)t0 = time.perf_counter()# 通过闭包访问自由变量 funcresult = func(*args) # 计算函数执行耗时(结束时间 - 开始时间)elapsed = time.perf_counter() - t0# 获取原始函数的名称name = func.__name__# 将所有位置参数转换为可打印的字符串表示arg_str = ', '.join(repr(arg) for arg in args)# 打印格式化的调用日志:# [耗时] 函数名(参数) -> 返回值# 示例:[0.12363791s] snooze(0.123) -> Noneprint(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')# 返回原始函数的计算结果,确保装饰后的函数行为与原函数一致return result# 返回内部函数,替换原函数# 导致内部函数完全取代原函数 从而导致原函数性质丢失return clocked
import time
from clockdeco0 import clock# @clock 语法等价于 factorial = clock(factorial)
@clock
def snooze(seconds):time.sleep(seconds)@clock
def factorial(n):return 1 if n < 2 else n * factorial(n - 1)if __name__ == '__main__':print('*' * 40, 'Calling snooze(.123)')snooze(.123)print('*' * 40, 'Calling factorial(6)')print('6! =', factorial(6))
**************************************** Calling snooze(.123)
[0.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
...
[0.00008297s] factorial(6) -> 720
6! = 720
保留元数据
基础版本不支持关键字参数(**kwargs
);覆盖了被装饰函数的 __name__
、__doc__
等元数据。
import time
import functoolsdef clock(func):@functools.wraps(func) # 保留原函数元数据def clocked(*args, **kwargs): # 支持关键字参数t0 = time.perf_counter()result = func(*args, **kwargs)elapsed = time.perf_counter() - t0name = func.__name__arg_lst = [repr(arg) for arg in args]arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())arg_str = ', '.join(arg_lst)print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')return resultreturn clocked
@functools.wraps(func)
将 func
的 __name__
、__doc__
、__module__
等属性复制到 clocked
,避免元数据丢失。*args, **kwargs
:使装饰器能处理任意位置参数和关键字参数。
装饰器的本质
装饰器动态地为函数附加额外职责(如日志、计时、缓存等);实现方式是用新函数替换原函数,新函数接收相同参数;执行额外逻辑;返回原函数应有的结果。
标准库中的装饰器
使用 functools.cache
实现记忆化
记忆化(memoization)即缓存函数调用结果,避免对相同参数重复计算。functools.cache要求所有参数必须可哈希(因为缓存键基于参数元组)。
# 未缓版本斐波那契数列
from clockdeco import clock@clock
def fibonacci(n):if n < 2:return nreturn fibonacci(n - 2) + fibonacci(n - 1)
# fibonacci(30)需要调用2,692,537次
[!NOTE]
print(fibonacci(6))[0.00000042s] fibonacci(0) -> 0 [0.00000049s] fibonacci(1) -> 1 [0.00006115s] fibonacci(2) -> 1 [0.00000031s] fibonacci(1) -> 1 ... [0.00016852s] fibonacci(6) -> 8 8 # fibonacci(1) 被调用了 8 次 # fibonacci(2) 调用了 5 次
# 缓存版本
import functools
from clockdeco import clock@functools.cache
@clock
def fibonacci(n):if n < 2:return nreturn fibonacci(n - 2) + fibonacci(n - 1)
# fibonacci(30) 仅调用31 次
# 使用升级版本
$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8
堆叠装饰器顺序:从下到上应用
@cache
+@clock
等价于fibonacci = cache(clock(fibonacci))
→ 先包装为clocked
,再对clocked
应用缓存。
但是要注意@cache
无大小限制,可能耗尽内存。其适合短生命周期脚本;长期运行服务应使用 @lru_cache
。
使用 functools.lru_cache
@functools.cache
实际是 @lru_cache(maxsize=None)
的别名。@lru_cache
支持maxsize
:缓存最大条目数(默认 128,建议设为 2 的幂);typed
:若为 True
,则 1
和 1.0
被视为不同键(默认为False)。
# Python 3.8+
@lru_cache
def f(x): ...# Python 3.2+
@lru_cache()
def f(x): ...# 自定义参数
@lru_cache(maxsize=2**20, typed=True)
def f(x): ...
单分派泛型函数@singledispatch
Python 无方法重载机制。传统方案(if/elif
或 match/case
)不可扩展,且难以维护。@singledispatch
允许模块化注册专用实现,支持第三方类型。
核心机制在于被 @singledispatch
装饰的函数成为泛型函数入口。运行时根据第一个参数的类型动态选择实现(单分派)。基础函数自动注册为 object
类型(兜底实现)。
from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers# 使用 @singledispatch 将 htmlize 定义为单分派泛型函数的默认实现
# 此函数处理所有未被其他专用函数覆盖的类型(兜底实现)
@singledispatch
def htmlize(obj: object) -> str:"""默认实现:对任意对象调用 repr(),进行 HTML 转义后包裹在 <pre> 标签中"""content = html.escape(repr(obj)) # 转义特殊字符(如 <, >, &, " 等)return f'<pre>{content}</pre>'# 为 str 类型注册专用实现
@htmlize.register
def _(text: str) -> str:"""处理字符串:转义后将换行符 \n 替换为 <br/>\n,并用 <p> 标签包裹"""# 注意:html.escape 会转义 &、<、> 等,确保内容安全content = html.escape(text).replace('\n', '<br/>\n')return f'<p>{content}</p>'# 为 abc.Sequence(如 list、tuple)注册专用实现
# 注意:str 也是 Sequence,但由于 str 的注册更具体,会优先匹配 str 实现
@htmlize.register
def _(seq: abc.Sequence) -> str:"""处理序列类型(如 list、tuple):递归格式化每个元素,生成 HTML 无序列表"""# 对序列中每个元素递归调用 htmlize,获得其 HTML 表示inner = '</li>\n<li>'.join(htmlize(item) for item in seq)return '<ul>\n<li>' + inner + '</li>\n</ul>'# 为 numbers.Integral(如 int、numpy 整数等)注册专用实现
@htmlize.register
def _(n: numbers.Integral) -> str:"""处理整数类型:显示十进制值和对应的十六进制形式(如 42 → 42 (0x2a))"""return f'<pre>{n} (0x{n:x})</pre>'# 为 bool 类型注册专用实现
# 注意:bool 是 int 的子类,也是 numbers.Integral 的子类,
# 但 singledispatch 会优先选择最具体的类型(bool),因此此实现生效
@htmlize.register
def _(n: bool) -> str:"""处理布尔值:仅显示 True/False,不显示十六进制(与 int 区分)"""return f'<pre>{n}</pre>'# 显式为 fractions.Fraction 类型注册专用实现(无类型注解,直接传入类型)
@htmlize.register(fractions.Fraction)
def _(x) -> str:"""处理 Fraction 对象:直接显示为“分子/分母”形式(如 Fraction(2, 3) → 2/3)"""frac = fractions.Fraction(x) # 确保输入为 Fraction(防御性转换)return f'<pre>{frac.numerator}/{frac.denominator}</pre>'# 同一实现同时注册给 decimal.Decimal 和 float 类型
@htmlize.register(decimal.Decimal)
@htmlize.register(float)
def _(x) -> str:"""处理浮点数和 Decimal:显示原始值,并附加一个近似分数表示"""# 使用 Fraction(x).limit_denominator() 获取最接近的简单分数frac = fractions.Fraction(x).limit_denominator()return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'
带参数的装饰器
当装饰器需要接收配置参数(如 @lru_cache(maxsize=128)
)时,不能直接将参数传给装饰器函数。
解决方法是:使用“装饰器工厂”模式,一个返回真正装饰器的函数。
装饰器工厂 ≠ 装饰器。
@decorator(...)
的执行顺序是:先调用decorator(...)
得到装饰器,再用该装饰器装饰函数。
基本结构
带参数装饰器通常包含三层函数嵌套:
- 最外层(装饰器工厂)
接收用户传入的配置参数(如active=True
,fmt=...
)。 - 中间层(真正的装饰器)
接收被装饰的函数func
。 - 最内层(包装函数,可选)
替换原函数,添加额外逻辑(如计时、缓存、日志),并返回原函数结果。
若装饰器仅做副作用(如注册),可省略包装层,直接返回 func
。
示例
registry = set()
# 装饰器工厂
# 根据配置参数 active,动态决定是否将被装饰的函数注册(加入或移除)到一个全局注册表(registry)中。
def register(active=True):# 真正的装饰器def decorate(func): print(f'running register(active={active})->decorate({func})')if active:registry.add(func)else:# 安全移除registry.discard(func) # 直接返回原函数return func # 返回装饰器return decorate
@register(active=False)
def f1(): pass@register() # 等价于 @register(active=True)
def f2(): pass
必须写 @register()
(带括号),否则 @register
会把函数对象 register
当作装饰器,导致类型错误。
同时支持运行时动态注册:
register()(f3) # 注册 f3
# # 等价于 @register() def f3(): ...
register(active=False)(f2) # 取消注册 f2
示例 2 带参数的装饰器
import timeDEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'# 装饰器工厂 接受格式字符串
def clock(fmt=DEFAULT_FMT):# 装饰器:接收函数def decorate(func):# 包装函数:拦截调用def clocked(*_args): t0 = time.perf_counter()_result = func(*_args)elapsed = time.perf_counter() - t0name = func.__name__args = ', '.join(repr(arg) for arg in _args)result = repr(_result)# 动态格式化print(fmt.format(**locals()))return _resultreturn clockedreturn decorate
@clock() # 默认格式
def snooze(seconds): time.sleep(seconds)@clock('{name}: {elapsed:.3f}s') # 自定义格式
def work(n): ...
利用 **locals()
将局部变量(elapsed
, name
, args
, result
)注入格式化上下文,极大提升灵活性。虽然静态分析工具可能警告“变量未使用”,但这是 Python 动态特性的合理应用。
[!NOTE]
在
clocked
函数体内,以下局部变量已被定义:t0 = time.perf_counter() _result = func(*_args) elapsed = time.perf_counter() - t0 name = func.__name__ args = ', '.join(repr(arg) for arg in _args) result = repr(_result)
因此,调用
locals()
时,会得到一个类似这样的字典(实际值取决于运行时):{'t0': 123456.789,'_result': None,'elapsed': 0.12345678,'name': 'snooze','args': '0.123','result': 'None','_args': (0.123,),'func': <function snooze at 0x...>,'fmt': '[{elapsed:0.8f}s] {name}({args}) -> {result}' }
注意:
fmt
之所以也在locals()
中,是因为它是从外层作用域(decorate
)闭包捕获的变量,在clocked
中可读,因此也被包含在locals()
返回结果中(CPython 实现中,闭包变量在locals()
中可见)。因为
fmt
是一个格式字符串,例如:'[{elapsed:0.8f}s] {name}({args}) -> {result}'
当执行:
fmt.format(**locals())
等价于:
fmt.format(elapsed=0.12345678,name='snooze',args='0.123',result='None',# ... 其他变量也传入,但 format 只用到需要的 )
Python 的
str.format()
会自动从关键字参数中查找{elapsed}
、{name}
等占位符对应的值。即使
locals()
包含了多余变量(如t0
,_result
),format()
也会忽略未使用的键,只要所需字段都存在就不会报错。 所以,**locals()
能成功提供fmt.format()
所需的所有命名字段。
示例 3 基于类的实现
import timeDEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'class clock: # 小写类名:强调其作为函数式装饰器的替代def __init__(self, fmt=DEFAULT_FMT):self.fmt = fmt # 保存配置def __call__(self, func): # 使实例可调用,充当装饰器def clocked(*_args):t0 = time.perf_counter()_result = func(*_args)elapsed = time.perf_counter() - t0name = func.__name__args = ', '.join(repr(arg) for arg in _args)result = repr(_result)print(self.fmt.format(**locals()))return _resultreturn clocked
当前实现的优点在于状态管理清晰,配置参数(fmt
)作为实例属性存储。同时易于扩展,可轻松添加 __wrapped__
、cache_clear()
等接口。符合 Python 习惯,许多标准库装饰器(如 functools.lru_cache
)内部也采用类实现。