装饰器
装饰器是一种接收函数并返回新函数的高阶函数。它可以在不修改原函数主体的情况下,为函数增加日志、计时、权限检查、缓存或重试等通用行为。
装饰器的核心只有一句话:用一个可调用对象包装另一个可调用对象。理解函数可以被传入和返回,就能理解装饰器。
本文使用 Python 3.10 及以上版本。读完后,你应该能够手写基础装饰器,保留被装饰函数的元数据,并正确处理参数、返回值和多个装饰器的顺序。
从普通包装函数开始
先不使用特殊语法,手动包装一个函数:
def greet(name):
return f"你好,{name}"
def add_logging(function):
def wrapper(name):
print(f"调用 {function.__name__}")
result = function(name)
print(f"返回 {result}")
return result
return wrapper
logged_greet = add_logging(greet)
print(logged_greet("小林"))
add_logging(greet) 执行一次,返回 wrapper;之后每次调用 logged_greet 都会先执行日志,再调用原来的 greet。原函数和包装函数都可以被赋值、传递或再次包装。
@ 语法
Python 提供 @装饰器 语法,把“定义后立即包装”写得更直观:
@add_logging
def greet(name):
return f"你好,{name}"
它等价于:
def greet(name):
return f"你好,{name}"
greet = add_logging(greet)
装饰发生在模块执行到函数定义时,而不是第一次调用函数时。因此装饰器本身需要的配置、导入和副作用会在定义阶段发生。
正确转发任意参数
只接收 name 的包装器只能装饰同样签名的函数。通用装饰器应使用 *args 和 **kwargs 转发位置参数与关键字参数:
from collections.abc import Callable
from functools import wraps
from typing import Any
def add_logging(function: Callable[..., Any]) -> Callable[..., Any]:
@wraps(function)
def wrapper(*args: Any, **kwargs: Any) -> Any:
print(f"调用 {function.__name__}")
result = function(*args, **kwargs)
print(f"返回 {result}")
return result
return wrapper
@add_logging
def add(left: int, right: int = 0) -> int:
return left + right
print(add(2, right=3)) # 5
包装器必须返回原函数的结果,否则调用者会得到 None。如果要修改参数或返回值,要明确写出这种 API 变化,并为它添加测试。
为什么使用 functools.wraps
直接包装会丢失原函数的名称、文档字符串和类型相关元数据:
def plain_decorator(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
functools.wraps 会把重要元数据复制到 wrapper,并设置 __wrapped__,方便调试、文档工具和测试框架访问原函数:
from functools import wraps
def identity_decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
@identity_decorator
def greet(name):
"""返回问候语。"""
return f"你好,{name}"
print(greet.__name__) # greet
print(greet.__doc__) # 返回问候语。
只要包装器替代了原函数,就应优先使用 @wraps(function)。
常用装饰器示例
计时
from functools import wraps
from time import perf_counter
def timed(function):
@wraps(function)
def wrapper(*args, **kwargs):
started = perf_counter()
try:
return function(*args, **kwargs)
finally:
elapsed = perf_counter() - started
print(f"{function.__name__} 用时 {elapsed:.4f}s")
return wrapper
使用 try...finally 可以确保原函数抛出异常时也记录耗时。计时输出适合诊断,不应在库代码中无条件打印;生产环境通常接入日志系统。
权限检查
from functools import wraps
def requires_role(expected_role):
def decorator(function):
@wraps(function)
def wrapper(user, *args, **kwargs):
if user.get("role") != expected_role:
raise PermissionError("没有执行该操作的权限")
return function(user, *args, **kwargs)
return wrapper
return decorator
@requires_role("admin")
def delete_user(user, user_id):
return f"已删除用户 {user_id}"
这里有两层函数:requires_role("admin") 先接收配置并返回 decorator,decorator 再接收被装饰函数并返回 wrapper。带参数装饰器本质上是“返回装饰器的函数”。
多个装饰器的顺序
多个装饰器从上到下书写,但从下到上应用:
@first
@second
def task():
pass
等价于:
task = first(second(task))
调用时则先经过 first 的包装器,再进入 second 的包装器,最后执行 task。日志、缓存和权限检查叠加时,顺序会影响行为,应在测试中明确验证。
类装饰器与方法
装饰器接收的是类对象时,也可以修改或替换整个类,但这会影响继承、调试和类型检查,只有确有必要时才使用。装饰实例方法时,包装器的 *args 中通常包含 self:
from functools import wraps
def announce(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"执行 {function.__name__}")
return function(*args, **kwargs)
return wrapper
class Calculator:
@announce
def add(self, left, right):
return left + right
不要在装饰器中假设所有函数都有 self 或固定参数名;通用包装器应透明地转发参数。
异步函数的注意事项
普通装饰器返回的 wrapper 如果是普通函数,装饰 async def 时只会返回协程对象,不能在其中直接 await 原函数。异步装饰器需要定义 async def wrapper:
from functools import wraps
def async_announce(function):
@wraps(function)
async def wrapper(*args, **kwargs):
print(f"开始 {function.__name__}")
return await function(*args, **kwargs)
return wrapper
同步和异步函数的装饰器不能不加区分地混用;应根据被装饰函数的调用协议选择实现。
常见错误与边界
- 忘记返回结果:包装器没有
return function(...),调用者会得到None。 - 忘记
wraps:名称和文档变成wrapper,调试与工具支持变差。 - 在装饰阶段执行本应延迟到调用阶段的逻辑:
@decorator下面的函数定义一执行就会包装。 - 共享可变状态:装饰器闭包中的列表或字典会被所有调用共享,必须明确是否需要线程安全。
- 过度装饰:装饰器层层嵌套后,调用链难以追踪;复杂流程可以改用显式组合或普通函数。
练习
- 编写
log_call,记录函数名、位置参数和关键字参数。 - 编写带参数的
retry(times),在指定次数内重试抛出异常的函数。 - 给一个装饰器补上
wraps,比较__name__和__doc__的变化。 - 创建两个简单装饰器,交换它们的书写顺序并观察调用日志。
小结
- 装饰器是“接收函数并返回函数”的高阶函数。
@decorator等价于在函数定义后执行function = decorator(function)。- 使用
*args、**kwargs转发参数,并返回原函数结果;使用functools.wraps保留元数据。 - 带参数装饰器需要额外一层函数;多个装饰器从下到上应用。
- 装饰异步函数时,包装器也应是
async def并await原函数。