跳到主要内容

装饰器

装饰器是一种接收函数并返回新函数的高阶函数。它可以在不修改原函数主体的情况下,为函数增加日志、计时、权限检查、缓存或重试等通用行为。

装饰器的核心只有一句话:用一个可调用对象包装另一个可调用对象。理解函数可以被传入和返回,就能理解装饰器。

本文使用 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 下面的函数定义一执行就会包装。
  • 共享可变状态:装饰器闭包中的列表或字典会被所有调用共享,必须明确是否需要线程安全。
  • 过度装饰:装饰器层层嵌套后,调用链难以追踪;复杂流程可以改用显式组合或普通函数。

练习​

  1. 编写 log_call,记录函数名、位置参数和关键字参数。
  2. 编写带参数的 retry(times),在指定次数内重试抛出异常的函数。
  3. 给一个装饰器补上 wraps,比较 __name__ 和 __doc__ 的变化。
  4. 创建两个简单装饰器,交换它们的书写顺序并观察调用日志。

小结​

  • 装饰器是“接收函数并返回函数”的高阶函数。
  • @decorator 等价于在函数定义后执行 function = decorator(function)。
  • 使用 *args、**kwargs 转发参数,并返回原函数结果;使用 functools.wraps 保留元数据。
  • 带参数装饰器需要额外一层函数;多个装饰器从下到上应用。
  • 装饰异步函数时,包装器也应是 async def 并 await 原函数。