Python 带参数装饰器怎么写?多个装饰器的执行顺序是什么?
简化版
带参数装饰器需要三层函数:最外层接收装饰器参数,中间层接收被装饰函数,最内层 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)
因此普通装饰器只需要:
- 外层
log接收原函数; - 内层
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 是被装饰函数,user 和 user_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)):装饰应用自下而上,实际调用自外而内。遇到权限、缓存、事务时,顺序要按业务语义仔细设计。