多个装饰器一起使用(装饰器叠加)

5 阅读6分钟

好的,以下是对 多个装饰器一起使用 的更详细总结,包含原理图解、执行流程分析、顺序影响、带参数装饰器叠加、保留元信息、常见错误以及实战示例。


多个装饰器叠加使用(详细版)

一、基本语法与等价形式

@deco1
@deco2
@deco3
def func():
    pass

完全等价于

func = deco1( deco2( deco3(func) ) )
  • 装饰器书写顺序:从上到下(deco1deco2deco3
  • 装饰器调用顺序:从下到上(先 deco3,再 deco2,最后 deco1

二、装饰阶段与调用阶段(详细)

2.1 装饰阶段(定义函数时)

当 Python 执行到 @deco1 等语句时,它立即计算装饰器表达式,并从最内层开始逐层包装

步骤分解(以 @deco1 @deco2 @deco3 func 为例):

  1. 计算 deco3(func),得到 wrapped3(假设 deco3 返回一个包装函数)。
  2. 计算 deco2(wrapped3),得到 wrapped2
  3. 计算 deco1(wrapped2),得到 wrapped1
  4. 将名称 func 绑定到 wrapped1

结果func 现在指向最外层装饰器 deco1 返回的包装函数。

2.2 调用阶段(执行 func() 时)

此时实际执行的是最外层包装函数 wrapped1。其内部结构大致如下:

# wrapped1 内部(由 deco1 返回)
def wrapped1(*args, **kwargs):
    # deco1 的前置代码
    result = wrapped2(*args, **kwargs)   # wrapped2 是 deco2 返回的包装函数
    # deco1 的后置代码
    return result

wrapped2 内部又会调用 wrapped3wrapped3 内部调用原始 func

执行流程就像一个“洋葱”:从最外层进入,逐层深入,到达最内层(原函数),然后逐层退出。


三、执行顺序图解(洋葱模型)

调用 func()
    │
    ▼
┌─────────────────────────────┐
│  deco1 的前置代码(先执行)     │
│  ┌─────────────────────────┐  │
│  │ deco2 的前置代码          │  │
│  │ ┌─────────────────────┐  │  │
│  │ │ deco3 的前置代码      │  │  │
│  │ │ ┌───────────────┐   │  │  │
│  │ │ │  原始 func     │   │  │  │
│  │ │ └───────────────┘   │  │  │
│  │ │ deco3 的后置代码      │  │  │
│  │ └─────────────────────┘  │  │
│  │ deco2 的后置代码          │  │
│  └─────────────────────────┘  │
│  deco1 的后置代码(最后执行)     │
└─────────────────────────────┘

输出顺序(假设每个装饰器打印“开始”和“结束”):

  1. deco1 开始
  2. deco2 开始
  3. deco3 开始
  4. 原始函数执行
  5. deco3 结束
  6. deco2 结束
  7. deco1 结束

四、完整示例(带打印追踪)

def deco1(func):
    def wrapper(*args, **kwargs):
        print(">>> deco1: 前置代码")
        result = func(*args, **kwargs)
        print("<<< deco1: 后置代码")
        return result
    return wrapper

def deco2(func):
    def wrapper(*args, **kwargs):
        print("   >>> deco2: 前置代码")
        result = func(*args, **kwargs)
        print("   <<< deco2: 后置代码")
        return result
    return wrapper

def deco3(func):
    def wrapper(*args, **kwargs):
        print("      >>> deco3: 前置代码")
        result = func(*args, **kwargs)
        print("      <<< deco3: 后置代码")
        return result
    return wrapper

@deco1
@deco2
@deco3
def say_hello():
    print("         [原函数] Hello!")

say_hello()

输出

>>> deco1: 前置代码
   >>> deco2: 前置代码
      >>> deco3: 前置代码
         [原函数] Hello!
      <<< deco3: 后置代码
   <<< deco2: 后置代码
<<< deco1: 后置代码

(缩进只是为了视觉层次)


五、装饰器顺序对结果的影响

顺序不同,行为可能完全不同。举例:权限校验 + 日志记录

5.1 正确的顺序(先校验,后记录)

@log        # 2. 记录调用日志
@require_login  # 1. 先检查登录
def profile():
    pass

等价于:profile = log( require_login(profile) )

  • 调用时先执行 log 的前置代码(记录开始时间等),然后调用 require_login 包装的函数,require_login 先校验登录,通过后才调用原函数。

5.2 错误的顺序(先记录,后校验)

@require_login
@log
def profile():
    pass

等价于:profile = require_login( log(profile) )

  • 调用时先执行 require_login 的前置代码(校验登录),但此时还没有记录日志。如果登录失败,日志就不会被记录,可能丢失审计信息。

结论:根据功能逻辑选择顺序,通常“核心功能”在内层,“外围增强”在外层。


六、带参数的多个装饰器叠加

带参数的装饰器本身是三层嵌套,叠加时仍然遵循从下到上的装饰顺序。

示例:重试 + 超时

import time
import functools

def retry(times=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if i == times - 1:
                        raise e
                    print(f"重试 {i+1}")
        return wrapper
    return decorator

def timeout(seconds):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            import signal
            def handler(signum, frame):
                raise TimeoutError(f"执行超时 {seconds} 秒")
            signal.signal(signal.SIGALRM, handler)
            signal.alarm(seconds)
            try:
                return func(*args, **kwargs)
            finally:
                signal.alarm(0)
        return wrapper
    return decorator

@retry(times=2)
@timeout(seconds=1)
def risky_task():
    time.sleep(2)
    return "Done"

# 调用时:
# 1. retry 的外层包装先执行
# 2. 进入 retry 内部后,调用 timeout 返回的包装函数
# 3. timeout 内部设置闹钟,然后调用原函数
# 4. 原函数超时抛出 TimeoutError,被 retry 捕获并重试一次
# 5. 第二次依然超时,最终抛出异常

装饰顺序:先 @timeout 装饰原函数,再 @retry 装饰结果。
执行顺序:先进入 retry 的包装,再进入 timeout 的包装。


七、保留元信息:functools.wraps

多个装饰器叠加后,原函数的 __name____doc__ 等属性会丢失,变成最外层包装函数的属性。使用 @functools.wraps(func) 可以解决。

未使用 wraps

def deco1(func):
    def wrapper():
        """wrapper doc"""
        return func()
    return wrapper

@deco1
def hello():
    """hello doc"""
    pass

print(hello.__name__)   # 输出 'wrapper'
print(hello.__doc__)    # 输出 'wrapper doc'

使用 wraps

import functools

def deco1(func):
    @functools.wraps(func)
    def wrapper():
        """wrapper doc"""
        return func()
    return wrapper

@deco1
def hello():
    """hello doc"""
    pass

print(hello.__name__)   # 输出 'hello'
print(hello.__doc__)    # 输出 'hello doc'

建议:每个装饰器内部都加上 @functools.wraps(func),这样叠加多个后,原函数的信息依然保留,对调试和文档生成很有帮助。


八、常见错误与注意事项

8.1 装饰器忘记返回包装函数

def deco(func):
    def wrapper():
        return func()
    # 忘记 return wrapper   → 原函数被替换为 None

8.2 顺序误解

以为 @deco1 @deco2 就是先执行 deco1 再执行 deco2
正确:装饰阶段先 deco2deco1;调用阶段先 deco1 的前置,再 deco2 的前置。

8.3 带参数装饰器少写一层

def retry(times):   # 缺少内层 decorator 函数
    def wrapper(func):
        ...
    return wrapper   # 实际上 retry 直接返回了 wrapper,少了一层

标准带参数装饰器需要三层:外层接收参数,中层接收函数,内层接收调用参数。

8.4 多个装饰器同时修改返回值

如果装饰器修改了返回值(例如将字符串转为大写),后面的装饰器将基于修改后的值操作,可能产生意外。

def upper_result(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

def add_exclamation(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) + "!"
    return wrapper

@add_exclamation
@upper_result
def greet():
    return "hello"

print(greet())   # 输出 "HELLO!"  (先转大写,再加感叹号)

若交换顺序:@upper_result @add_exclamation 则先加感叹号再转大写 → "HELLO!" 相同(因为大写不影响感叹号)。但若涉及数字等类型转换则可能出错。


九、实际应用场景举例

组合说明
@cache + @log先缓存,缓存命中时不记录日志?通常先记录日志再查缓存,以便追踪调用。
@login_required + @admin_required先检查是否登录,再检查是否是管理员。
@retry + @timeout先设置超时,再重试(超时异常可以被重试捕获)。
@flask_route + @jwt_requiredWeb 框架中先匹配路由,再验证 JWT。

十、总结一句话

装饰器叠加时,书写顺序从上到下,装饰顺序从下到上;调用时从最外层进入,逐层深入原函数,再逐层退出,如同洋葱。

掌握这个模型,无论多少个装饰器、带不带参数,都能准确预测执行流程。