装饰器从下往上加载、从上往下执行:@deco_a@deco_b等价于f = deco_a(deco_b(f)),先加载deco_b再deco_a,调用时先执行deco_a外层逻辑,再deco_b,最后原函数。
Python 多装饰器的
执行顺序常让人困惑,关键要分清「定义时的加载顺序」和「调用时的执行顺序」。装饰器语法 @a、@b 写在函数上方,实际等价于 f = a(b(f)) —— 也就是说,@b 先被应用,再被 @a 包裹。
这意味着:
@b,再看到 @a)a 的外层逻辑,再进 b 的外层逻辑,最后到原函数)def deco_a(func):
print("deco_a applied")
def wrapper_a(*args, **kwargs):
print("→ in deco_a before")
result = func(*args, **kwargs)
print("← in deco_a after")
return result
return wrapper_a
def deco_b(func):
print("deco_b applied")
def wrapper_b(*args, *kwargs):
print("→ in deco_b before")
result = func(args, **kwargs)
print("← in deco_b after")
return result
return wrapper_b
@deco_a
@deco_b
def say_hello():
print("hello")
say_hello()
运行输出:
deco_b applied deco_a applied → in deco_a before → in deco_b before hello ← in deco_b after ← in deco_a after
像 @retry(max_tries=3) 这类装饰器,本质是「返回装饰器的工厂函数」,它比无参装饰器多执行一次函数调用。这会影响加载和执行的嵌套层级。
常见误区是以为 @retry(...) 和 @cache 是平级的——其实 retry(...) 先被调用,返回一个真正的装饰器,然后再参与上面的「下往上加载」流程。
@retry(max_tries=3) → 先执行 retry(max_tries=3),返回一个函数 decorator
decorator 才真正参与 @decorator + @cache 的叠加顺序f = decorator(cache(f)),而非 f = retry(...)(cache(f))(后者写法错误)顺序不是风格问题,直接决定控制流和错误处理边界。比如 @log 和 @auth 谁在外层,决定了日志里是否包含未通过鉴权的请求。
@log 在外层:所有调用(包括鉴权失败)都会被记录@auth 在外层:只有通过鉴权的请求才进入 @log,失败直接抛出异常,不进日志逻辑@cache 放最内层(靠近原函数)才能缓存业务结果;放最外层可能缓存了错误响应或日志时间戳@timeout 在最外层,它能中断整个装饰链(包括重试、日志等);若在内层,可能被 @retry 反复触发functools.wraps + 打印栈帧装饰器嵌套后,func.__name__ 和 help(func) 容易丢失原信息,导致调试困难。务必在每个包装函数里加 @wraps(func)。
更直接的办法是在各 wrapper 开头打印当前栈深度或装饰器名:
import inspectdef trace_deco(name): def deco(func): @wraps(func) def wrapper(*args, *kwargs): frame = len(inspect.stack()) - 10 # 粗略估算嵌套深度 print(f"[{name}] depth={frame}") return func(args, **kwargs) return wrapper return deco
这种输出能快速确认哪一层卡住、哪一层没执行——尤其当某个装饰器 silently 吞掉异常或提前 return 时。
最容易被忽略的是:装饰器里的 return func(...) 如果漏了 return,会导致外层装饰器收到 None,后续逻辑全错乱。这类 bug 不报错,只静默失效。