6月2日 01:38
Python 装饰器是怎么工作的?@ 语法糖、执行时机和 wraps 详解
装饰器的本质是一个接收函数作为参数并返回新函数的高阶函数。@decorator 语法糖等价于 func = decorator(func)。理解装饰器的关键:它只是函数替换,在定义时执行,不是调用时。
装饰器做了什么
pythondef 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()。
执行时机
pythondef 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:
pythondef 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:
pythonfrom 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 时都能访问到被装饰的原始函数。装饰器就是"创建闭包的工厂函数"。
追问
装饰器能装饰类吗?
能。装饰器接收的参数不一定是函数,也可以是类:
pythondef 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 缓存结果避免重复执行。