Skip to main content

装饰器

装饰器(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 完整转发调用参数;
  • 明确异常是透传、转换还是重试,不要静默吞掉异常;
  • 注意多个装饰器的包装和执行顺序;
  • 装饰器不要隐藏过多副作用,否则函数行为会难以推断;
  • 对同步函数和异步函数要分别处理,普通同步包装器不能直接替代异步包装器。

装饰器适合抽取稳定、重复且与核心业务相对独立的逻辑。只在单个函数中使用、与业务流程紧密耦合的代码,直接写在函数内部往往更清晰。