6月2日 01:38

Python 装饰器是怎么工作的?@ 语法糖、执行时机和 wraps 详解

装饰器的本质是一个接收函数作为参数并返回新函数的高阶函数。@decorator 语法糖等价于 func = decorator(func)。理解装饰器的关键:它只是函数替换,在定义时执行,不是调用时。

装饰器做了什么

python
def log(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @log def hello(name): print(f"Hello, {name}") # 等价于 hello = log(hello)

@log 发生在函数定义时,不是调用时。Python 解释器看到 @log 后,把 hello 传给 log(),返回的 wrapper 替换掉原来的 hello。之后所有对 hello() 的调用实际上调的是 wrapper()

执行时机

python
def log(func): print("装饰器执行了") # 模块加载时就执行 def wrapper(*args, **kwargs): print("wrapper 执行了") # 每次调用函数时执行 return func(*args, **kwargs) return wrapper @log def hello(): print("hello") # 输出: 装饰器执行了(定义时立即执行) hello() # 输出: wrapper 执行了 / hello

装饰器外层的代码在模块加载时执行(一次),wrapper 内的代码在每次函数调用时执行。

多个装饰器的叠加

python
@a @b def f(): pass # 等价于 f = a(b(f))

从下到上装饰。调用 f() 时,执行顺序是 a -> b -> f -> b 的后处理 -> a 的后处理。

带参数的装饰器

需要三层嵌套——最外层接收装饰器参数,中间层接收被装饰函数,最内层是 wrapper:

python
def retry(max_attempts=3): def decorator(func): def wrapper(*args, **kwargs): for i in range(max_attempts): try: return func(*args, **kwargs) except Exception: if i == max_attempts - 1: raise return wrapper return decorator @retry(max_attempts=5) def call_api(): ... # 等价于 call_api = retry(max_attempts=5)(call_api)

@retry(5) 先调用 retry(5) 返回 decorator,再 decorator(call_api) 返回 wrapper。三层嵌套是固定模式。

functools.wraps

装饰器替换了原函数,导致 __name____doc__ 等元信息丢失。@wraps(func) 复制原函数的元信息到 wrapper:

python
from functools import wraps def log(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @log def hello(): pass print(hello.__name__) # hello(没有 @wraps 会是 wrapper)

装饰器 vs 闭包

装饰器是基于闭包的。wrapper 闭包了 func 变量——每次调用 wrapper 时都能访问到被装饰的原始函数。装饰器就是"创建闭包的工厂函数"。

追问

装饰器能装饰类吗?

能。装饰器接收的参数不一定是函数,也可以是类:

python
def add_repr(cls): def __repr__(self): return f"{cls.__name__}({self.__dict__})" cls.__repr__ = __repr__ return cls @add_repr class User: def __init__(self, name): self.name = name

dataclass 的 @dataclass 就是类装饰器。

装饰器有性能开销吗?

有一层函数调用的开销,通常可以忽略。但在极高性能场景(百万次/秒调用),装饰器的额外调用栈可能成为瓶颈。可以用 functools.lru_cache 缓存结果避免重复执行。

标签:Python