装饰器
装饰器(Decorator)是一种在不直接修改原函数或类主体的情况下,为其增加额外行为的机制。Python 中函数和类都是对象,可以作为参数传递、作为返回值返回,因此一个函数可以接收另一个函数,对其进行包装后再返回新函数。
装饰器语法:
@decorator
def target():
pass
大致等价于:
def target():
pass
target = decorator(target)
装饰器通常把日志、权限、缓存、计时等横切逻辑集中到一个位置,让业务函数只关注自身职责。
基本装饰器
包装无参数函数
from functools import wraps
def log_call(function):
@wraps(function)
def wrapper():
print(f"calling {function.__name__}")
result = function()
print(f"finished {function.__name__}")
return result
return wrapper
@log_call
def greet():
return "Hello"
print(greet())
调用 greet() 时,实际执行的是 wrapper()。包装函数在调用原函数之前和之后插入了日志,并保留原函数返回值。
包装任意参数函数
通用装饰器通常使用 *args 和 **kwargs 转发参数:
from functools import wraps
def log_call(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"calling {function.__name__}")
return function(*args, **kwargs)
return wrapper
@log_call
def add(left, right=0):
return left + right
print(add(2, right=3)) # 5
*args 接收所有位置参数,**kwargs 接收所有关键字参数,再原样传给被装饰函数。
使用 functools.wraps
如果不使用 functools.wraps,装饰后的函数名称、文档字符串和部分注解会变成包装函数的信息:
print(add.__name__) # 使用 wraps 时仍为 add
@wraps(function) 会复制重要元数据,并通过 __wrapped__ 保留对原函数的引用。这对日志、调试、帮助文档、类型工具和测试都很重要,因此函数装饰器一般都应使用它。
带参数的装饰器
如果装饰器本身需要配置,可以再增加一层函数:
from functools import wraps
def repeat(times):
if times < 1:
raise ValueError("times must be at least 1")
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = function(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say(message):
print(message)
say("hello")
执行顺序可以理解为:
say = repeat(times=3)(say)
三层函数各自负责:
repeat(times):接收装饰器配置;decorator(function):接收被装饰函数;wrapper(*args, **kwargs):接收调用函数时的参数。
多个装饰器
一个函数可以同时使用多个装饰器:
@decorator_a
@decorator_b
def process():
pass
装饰发生时等价于:
process = decorator_a(decorator_b(process))
因此,靠近函数的 decorator_b 先包装,decorator_a 后包装。实际调用时通常先进入最外层的 decorator_a,再进入 decorator_b。当装饰器会改变参数、返回值或异常时,顺序可能影响最终行为。
常用场景
记录日志
from functools import wraps
def audit(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"audit: {function.__name__}, args={args}, kwargs={kwargs}")
return function(*args, **kwargs)
return wrapper
日志装饰器适合统一记录函数调用、参数和执行结果。真实项目中要避免把密码、令牌等敏感参数直接写入日志。
统计耗时
from functools import wraps
from time import perf_counter
def timer(function):
@wraps(function)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
return function(*args, **kwargs)
finally:
elapsed = perf_counter() - start
print(f"{function.__name__}: {elapsed:.6f}s")
return wrapper
使用 finally 可以保证被装饰函数抛出异常时仍然记录耗时。
权限检查
from functools import wraps
def require_role(required_role):
def decorator(function):
@wraps(function)
def wrapper(user, *args, **kwargs):
if required_role not in user.roles:
raise PermissionError(f"role required: {required_role}")
return function(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(current_user, target_user_id):
return f"deleted {target_user_id}"
权限装饰器可以统一入口检查,但业务系统仍需要在可信的服务端执行授权,不能只依赖客户端装饰器。
缓存
标准库已经提供了缓存装饰器:
from functools import cache
@cache
def fibonacci(number):
if number < 2:
return number
return fibonacci(number - 1) + fibonacci(number - 2)
缓存要求参数可哈希,并会占用额外内存。对于依赖外部可变状态或带有副作用的函数,不能简单缓存返回值。
重试
from functools import wraps
def retry(max_attempts):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return function(*args, **kwargs)
except TimeoutError:
if attempt == max_attempts:
raise
return wrapper
return decorator
生产环境中的重试通常还需要限制可重试异常、增加退避和随机抖动,并确认操作具备幂等性。
注册函数
装饰器也可以在函数定义时将其加入注册表:
handlers = {}
def register(name):
def decorator(function):
handlers[name] = function
return function
return decorator
@register("created")
def handle_created(event):
return event["id"]
Web 路由、命令处理器、插件系统和序列化器经常采用这种模式。
类装饰器与内置装饰器
装饰器不仅可以作用于函数,也可以作用于类:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
@dataclass 会为类生成初始化、比较和字符串表示等常用方法。
类中常见的内置装饰器还包括:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@staticmethod
def freezing_point():
return 0
@classmethod
def from_fahrenheit(cls, fahrenheit):
return cls((fahrenheit - 32) * 5 / 9)
@property:把方法暴露为受控属性访问;@staticmethod:声明不依赖实例和类状态的方法;@classmethod:接收类对象,常用于替代构造函数。
为什么使用装饰器
装饰器适合解决多个函数都需要的横切关注点:
- 业务代码和通用功能相互分离;
- 同一套日志、认证、缓存等逻辑可以复用;
- 功能通过
@名称明确标注在函数上方; - 可以组合多个彼此独立的行为;
- 修改包装逻辑时不需要逐个改动业务函数。
例如,把权限检查散落在每个接口内部,容易出现遗漏或实现不一致;集中为装饰器后,可以统一维护规则。
注意事项
- 使用
functools.wraps保留原函数元数据; - 正确返回原函数结果,不要无意中丢失返回值;
- 使用
*args和**kwargs完整转发调用参数; - 明确异常是透传、转换还是重试,不要静默吞掉异常;
- 注意多个装饰器的包装和执行顺序;
- 装饰器不要隐藏过多副作用,否则函数行为会难以推断;
- 对同步函数和异步函数要分别处理,普通同步包装器不能直接替代异步包装器。
装饰器适合抽取稳定、重复且与核心业务相对独立的逻辑。只在单个函数中使用、与业务流程紧密耦合的代码,直接写在函数内部往往更清晰。