带参数装饰器必须是三层函数,因为@decorator(arg)先调用decorator(arg)返回真正的装饰器;第一层收装饰器参数,第二层收被装饰函数,第三层收原函数参数并执行逻辑;需用@functools.wraps(func)保留元信息。

带参数装饰器为什么必须是三层函数
因为 python 解释器在遇到 @decorator(arg) 时,会先执行 decorator(arg),期望它返回一个真正的装饰器(即接收函数并返回包装后函数的可调用对象)。所以最外层函数负责接收装饰器参数,中间层接收被装饰函数,最内层负责实际逻辑。
常见错误是只写两层,导致 TypeError: 'function' Object is not callable 或装饰器完全不生效。
- 第一层:接收装饰器参数(如
log_level),返回第二层函数 - 第二层:接收被装饰的函数
func,返回第三层函数 - 第三层:接收原函数的参数(
*args, **kwargs),执行前后逻辑,调用func
如何正确保留原函数的元信息
不加处理时,被装饰后的函数 __name__、__doc__ 都会变成内层包装函数的,这对调试、文档生成(如 sphinx)、反射(inspect.signature)都会造成干扰。
必须使用 @functools.wraps(func) 修饰第三层函数:
立即学习“Python免费学习笔记(深入)”;
from functools import wraps def with_retry(max_attempts=3): def decorator(func): @wraps(func) # ← 关键:把 func 的元信息复制给 wrapper def wrapper(*args, *kwargs): for i in range(max_attempts): try: return func(args, **kwargs) except Exception: if i == max_attempts - 1: raise return None return wrapper return decorator
带参数装饰器的常见误用场景
最容易出错的是混淆「装饰器参数」和「被装饰函数参数」,尤其在动态构造装饰器时。
- 把
@my_dec(a=1)(b=2)当作多级参数——实际不合法,Python 只支持一层括号调用 - 在第二层(接收
func的函数)里直接访问装饰器参数,却忘了它属于闭包外层,需确保作用域链完整 - 用类实现带参装饰器时,忘记在
__call__中区分:第一次调用传的是func还是参数?典型做法是检查第一个参数是否为函数类型 - 参数类型校验缺失,比如传入
None或负数给max_retries,应在第一层就抛出ValueError
装饰器参数支持延迟求值吗
可以,但必须把求值逻辑移到第三层(wrapper 内),否则参数会在装饰定义时(模块加载期)就被计算,而非每次调用时。
例如日志中想记录当前时间戳,如果写成:
def log_time(label=time.time()): # ❌ 错误:模块导入时就固定了 ...
应改为:
def log_time(label="run"): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() # ✅ 正确:每次调用才计算 result = func(*args, **kwargs) print(f"[{label}] took {time.time() - start:.2f}s") return result return wrapper return decorator
闭包变量的生命周期和求值时机,是带参装饰器里最易被忽略的复杂点。