← 返回题目列表

Python 带参数装饰器怎么写?多个装饰器的执行顺序是什么?

高频 困难 第 15 / 21 题 更新于 2026/07/27
带参数装饰器装饰器顺序闭包wraps

简化版

带参数装饰器需要三层函数:最外层接收装饰器参数,中间层接收被装饰函数,最内层 wrapper 接收原函数调用参数。多个装饰器装饰同一个函数时,应用顺序自下而上,调用顺序自外而内。

详细版

带参数装饰器示例:

from functools import wraps

def retry(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for _ in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as exc:
                    last_error = exc
            raise last_error
        return wrapper
    return decorator

使用:

@retry(3)
def fetch_data():
    ...

等价于:

fetch_data = retry(3)(fetch_data)

多个装饰器:

@a
@b
def f():
    ...

等价于:

f = a(b(f))

装饰器应用时先执行 b(f),再执行 a(...);调用 f() 时先进入 a 的 wrapper,再进入 b 的 wrapper,最后到原函数。

完整版教学

一、普通装饰器为什么是两层

普通装饰器:

def log(func):
    def wrapper(*args, **kwargs):
        print("before")
        return func(*args, **kwargs)
    return wrapper

它接收的是被装饰函数:

@log
def f():
    ...

等价于:

f = log(f)

因此普通装饰器只需要:

  1. 外层 log 接收原函数;
  2. 内层 wrapper 接收调用参数。

二、带参数装饰器为什么要三层

当你写:

@retry(3)
def f():
    ...

Python 会先计算 retry(3),它必须返回一个真正的装饰器,然后再装饰 f

所以结构变成:

def retry(times):          # 接收装饰器参数
    def decorator(func):   # 接收原函数
        def wrapper(*args, **kwargs):  # 接收调用参数
            ...
        return wrapper
    return decorator

调用关系是:

f = retry(3)(f)

这里 retry(3) 返回 decorator,再执行 decorator(f)

三、写一个限流或权限类装饰器

带参数装饰器很适合表达策略。

from functools import wraps

def require_role(role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if user.role != role:
                raise PermissionError("permission denied")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

使用:

@require_role("admin")
def delete_user(user, user_id):
    ...

"admin" 是装饰器参数,delete_user 是被装饰函数,useruser_id 是函数调用参数。

四、多个装饰器的应用顺序

看这个例子:

def a(func):
    print("apply a")
    def wrapper(*args, **kwargs):
        print("call a")
        return func(*args, **kwargs)
    return wrapper

def b(func):
    print("apply b")
    def wrapper(*args, **kwargs):
        print("call b")
        return func(*args, **kwargs)
    return wrapper

@a
@b
def f():
    print("call f")

定义函数时输出:

apply b
apply a

调用函数时输出:

call a
call b
call f

因为:

f = a(b(f))

先把原函数交给下面的 b,再把 b 返回的函数交给上面的 a

五、装饰器顺序为什么重要

假设有两个装饰器:

@cache
@check_permission
def get_data(user):
    ...

等价于:

get_data = cache(check_permission(get_data))

调用时先进入缓存。如果缓存命中,可能不会执行权限校验,这在某些业务里是危险的。

更安全的顺序可能是:

@check_permission
@cache
def get_data(user):
    ...

调用时先校验权限,再考虑缓存。

装饰器顺序不是格式问题,而是行为问题。涉及权限、事务、缓存、异常处理时尤其要谨慎。

六、类装饰器也是可选方案

带参数装饰器嵌套层数多时,可以用类让结构更清楚:

from functools import wraps

class Retry:
    def __init__(self, times):
        self.times = times

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for _ in range(self.times):
                try:
                    return func(*args, **kwargs)
                except Exception as exc:
                    last_error = exc
            raise last_error
        return wrapper

使用:

@Retry(3)
def fetch():
    ...

类装饰器的关键是 __call__,让对象可以像函数一样被调用。

七、常见误区与追问

心法:带参数装饰器先“配置”,再“装饰”,最后才“调用”。多个装饰器要分清定义期的应用顺序和运行期的 wrapper 进入顺序。

  • 误区:@retry(3) 是把 3 传给原函数。 3 传给的是最外层装饰器工厂 retry,它返回真正的装饰器;原函数是在下一步被 decorator(func) 接收。
  • 追问:普通装饰器和带参数装饰器能不能兼容一种写法? 可以通过判断第一个参数是否为可调用对象来兼容 @decorator@decorator(...),但实现更复杂,面试中先讲清两层/三层结构更重要。
  • 误区:多个装饰器的应用顺序和调用顺序一样。 @a @b def f 定义时先应用 b 再应用 a;调用时先进入 a 的 wrapper,再进入 b 的 wrapper,最后执行原函数。
  • 追问:权限和缓存哪个装饰器应该放外层? 通常权限校验应在缓存读取之前,避免用户绕过权限直接拿到缓存结果;但具体还要看缓存 key 是否已经包含用户身份和权限维度。
  • 误区:类装饰器一定比函数装饰器好。 类装饰器适合有较多配置或状态的场景;简单日志、计时、权限校验用闭包函数更轻量,关键是结构清楚并保留 wraps
  • 追问:装饰器参数什么时候求值? 在函数定义阶段求值,例如模块导入时就会执行 retry(3);如果参数表达式有副作用,也会在导入或定义时发生,而不是等函数调用时才发生。

八、加强记忆

普通装饰器是 func -> wrapper,带参数装饰器是 参数 -> decorator -> wrapper。多个装饰器记住 @a @b def f 等价于 f = a(b(f)):装饰应用自下而上,实际调用自外而内。遇到权限、缓存、事务时,顺序要按业务语义仔细设计。