跳到主要内容

Python 装饰器与包装器

装饰器接收一个对象,并返回装饰后名称所指向的对象;返回的可以是原对象,不一定是新对象。函数装饰器常返回一个包装函数,由它调用原函数。下面的 traced 就是这样:每次调用包装函数时,先打印原函数名称,再转交实参,最后返回原函数的结果。这里的类型注解描述了参数与结果的传递关系:P 代表原函数的参数,R 代表返回类型,Callable[P, R] 表示具有这些参数和返回类型的可调用对象。

from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def traced(function: Callable[P, R]) -> Callable[P, R]:
@wraps(function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"calling {function.__qualname__}")
return function(*args, **kwargs)
return wrapper

这里使用 functools.wraps。它负责保留原函数的元数据(如 __name__, __doc__)并暴露 __wrapped__ 属性。这对文档生成、调试器以及依赖元数据的其他装饰器至关重要。同时,通过 ParamSpecTypeVar,我们能在静态类型检查中保留被包装函数的原始签名。

求值时机与组合顺序

装饰器表达式在定义语句执行时立即求值,通常发生在模块导入阶段。因此,装饰器内部的注册逻辑或配置操作应让副作用可预测。

当多个装饰器堆叠时,执行顺序是从内向外(即从下往上):

@outer
@inner
def operation(): ...

# 等价于:operation = outer(inner(operation))

这意味着顺序直接影响行为。例如,在缓存、重试、鉴权、事务或日志场景中,@cache 放在 @retry 外面还是里面,结果可能截然不同。

带参数的装饰器(工厂模式)

如果装饰器需要配置参数,它实际上是一个“工厂函数”,返回真正的装饰器:

class TransientError(Exception):
pass

def retry(*, attempts: int):
if attempts < 1:
raise ValueError("attempts must be positive")

def decorate(function):
@wraps(function)
def wrapper(*args, **kwargs):
for attempt in range(attempts):
try:
return function(*args, **kwargs)
except TransientError:
if attempt == attempts - 1:
raise
return wrapper
return decorate

重试策略需要明确哪些异常可恢复,以及重复执行是否安全。通用装饰器无法自动推断这些业务规则。

设计边界与陷阱

  • 同步与异步不匹配:用同步包装器包裹异步函数,返回的是协程对象而非执行结果,调用者必须 await。生成器也有类似的生命周期管理问题。
  • 不要隐藏复杂逻辑:避免将关键的控制流、I/O 操作或宽泛的异常吞没隐藏在看似简单的装饰器语法背后。这会降低代码的可读性和可维护性。
  • lru_cache 的适用性:它要求参数可哈希,且仅在“复用结果在语义上是安全的”场景下适用。如果函数依赖可变的外部状态或时间敏感的结果,使用缓存会导致错误。
  • 并非所有装饰器都是包装器propertyclassmethodstaticmethodcontextmanagersingledispatch 虽然使用装饰器语法,但定义了完全不同的契约。不要试图用“包装函数”的思维去理解它们,应分别掌握各自的 API 语义。

调用装饰后的函数

带类型注解的示例使用 ParamSpec,需要 Python 3.10+。定义 traced 后可以运行:

@traced
def add(left: int, right: int = 1) -> int:
return left + right

assert add(2, right=3) == 5
assert add.__name__ == "add"
assert add.__wrapped__(2, 3) == 5

只有第一次调用会打印 calling add*args**kwargs 分别转发位置参数和关键字参数;返回被包装函数的结果,才能保留调用者期待的返回值。wraps 不会强制检查类型,也不会把同步包装器变成异步包装器。

重试示例中的 TransientError 是演示用的异常,实际使用时应换成操作可能抛出的、可恢复的具体异常。对生成器函数重试,只会重试创建生成器,不会捕获随后迭代时的异常;异步操作的异常同样在等待执行时才出现。退避和抖动适用于存在竞争的远程服务,不是每次本地重试的必要条件。首先要判断部分完成的操作能否安全重复。

参考资源

探索关联

这篇笔记还没有文档关联。

同主题的其他笔记 (46)

打开关联网络