文章目录

Python @functools.wraps保留被装饰函数元信息

发布于 2026-05-03 21:17:06 · 浏览 26 次 · 评论 0 条

Python @functools.wraps保留被装饰函数元信息

编写 Python 装饰器时,如果不做特殊处理,被装饰函数的元信息(如函数名、文档字符串、参数注解等)会被替换为装饰器内部包装函数的信息。这会导致调试困难、文档生成错误以及基于函数签名的操作失效。functools.wraps 正是用于解决这一问题的标准工具。


1. 创建基础测试环境

打开 你的代码编辑器或 IDE,新建 一个名为 test_wraps.py 的文件。后续的操作将在此文件中进行。


2. 编写一个无装饰器的原始函数

为了对比效果,首先定义一个标准的函数,包含完整的文档字符串和类型注解。

输入 以下代码:

def add(a: int, b: int) -> int:
    """
    计算两个整数的和。

    Args:
        a: 第一个整数
        b: 第二个整数

    Returns:
        两个整数的和
    """
    return a + b

# 测试原始函数的元信息
if __name__ == "__main__":
    print(f"函数名: {add.__name__}")
    print(f"文档字符串: {add.__doc__}")
    print(f"参数注解: {add.__annotations__}")

运行 脚本,观察控制台输出。此时 __name__add__doc__ 是定义的说明文档,一切正常。


3. 编写未使用 @wraps 的装饰器

现在编写一个简单的日志装饰器,它不使用 functools.wraps

添加 以下代码到 test_wraps.py

import time

# 一个不使用 @wraps 的装饰器
def simple_timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 执行耗时: {end - start:.4f} 秒")
        return result
    return wrapper

# 应用装饰器
@simple_timer
def subtract(a: int, b: int) -> int:
    """
    计算两个整数的差。

    Args:
        a: 被减数
        b: 减数

    Returns:
        两个整数的差
    """
    return a - b

# 测试被装饰后的函数元信息
print("\n--- 未使用 @wraps 的结果 ---")
print(f"函数名: {subtract.__name__}")
print(f"文档字符串: {subtract.__doc__}")
print(f"参数注解: {subtract.__annotations__}")

保存运行 脚本。

注意 输出结果中的异常:

  • 函数名 变成了 wrapper
  • 文档字符串 变成了 wrapper 的代码(通常是 None)。
  • 参数注解 变成了空字典 {}

这说明 subtract 函数的“身份”已经被 wrapper 函数完全覆盖,这对于依赖元信息的工具(如 Sphinx 文档生成、调试器)是灾难性的。


4. 引入 @functools.wraps 修复问题

修改 上一步的装饰器代码,引入 functools 模块,并在 wrapper 函数定义上方 添加 @wraps(func) 装饰器。

替换 simple_timer 相关代码如下:

import functools
import time

# 使用 @wraps 修复后的装饰器
def smart_timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 执行耗时: {end - start:.4f} 秒")
        return result
    return wrapper

# 应用修复后的装饰器
@smart_timer
def multiply(a: int, b: int) -> int:
    """
    计算两个整数的积。

    Args:
        a: 乘数
        b: 乘数

    Returns:
        两个整数的积
    """
    return a * b

# 测试修复后的函数元信息
print("\n--- 使用 @wraps 修复后的结果 ---")
print(f"函数名: {multiply.__name__}")
print(f"文档字符串: {multiply.__doc__}")
print(f"参数注解: {multiply.__annotations__}")

保存运行 脚本。

确认 输出结果:

  • 函数名 恢复为 multiply
  • 文档字符串 恢复为原本的中文说明。
  • 参数注解 恢复为 {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

5. 理解 @wraps 的工作原理

@functools.wraps 实际上是一个偏函数应用,它底层调用的是 functools.update_wrapper 函数。

当我们 调用 @functools.wraps(func) 时,它主要执行了以下操作:

  1. 复制 模块属性:将 func__module____name____qualname____annotations____doc__ 属性复制给 wrapper
  2. 更新 __dict__:将 func__dict__ 属性更新到 wrapper 中。
  3. 隐藏 原始函数:在 wrapper 上设置 __wrapped__ 属性,指向原始的 func 对象。

这一过程可以用以下流程图表示:

graph TD A["原始函数: func"] -->|被装饰| B["包装函数: wrapper"] C["@functools.wraps"] -->|执行 update_wrapper| B B -->|获得| D["元信息: __name__, __doc__ 等"] B -->|指向| E["__wrapped__ 属性 -> func"]

6. 利用 __wrapped__ 属性进行逆向查找

使用 @wraps 后,Python 会在包装函数上添加一个 __wrapped__ 属性。这对于调试或需要绕过装饰器直接访问原始函数的场景非常有用。

添加 以下测试代码:

print("\n--- 测试 __wrapped__ 属性 ---")
print(f"multiply 是 multiply.__wrapped__ 吗? {multiply is multiply.__wrapped__}")
print(f"原始函数名: {multiply.__wrapped__.__name__}")

运行 代码,可以看到虽然外部调用的是 multiply(即包装后的 wrapper),但通过 .__wrapped__ 我们依然可以触及原始的函数对象。


7. 处理带参数的装饰器

在实际开发中,装饰器往往需要接收参数(例如 @retry(times=3))。@wraps 同样适用于这种嵌套结构。

编写 一个带参数的装饰器示例:

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            # 这里的 __name__ 会正确显示为 func 的名字,而不是 wrapper_repeat
            print(f"准备执行 {func.__name__} {num_times} 次")
            result = None
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper_repeat
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    """向某人打招呼"""
    print(f"Hello, {name}!")

print("\n--- 带参数装饰器的元信息 ---")
print(f"函数名: {greet.__name__}")
print(f"文档字符串: {greet.__doc__}")
greet("Alice")

观察 输出,即使装饰器嵌套了三层(repeat -> decorator_repeat -> wrapper_repeat),最内层的 @functools.wraps(func) 依然成功将 greet 的元信息传递到了最外层。


8. 最佳实践检查清单

在编写生产环境代码时,遵循 以下清单以确保元信息处理的正确性:

  • 始终导入 import functools
  • 始终在 内部 wrapper 函数上 使用 @functools.wraps(func)
  • 检查 __wrapped__ 属性是否被正确设置,以便于调试。
  • 避免wrapper 函数中手动覆盖 __name____doc__,应完全交给 wraps 处理。

9. 常见错误排查

如果在使用 @wraps 后发现元信息依然丢失,排查 以下两点:

  1. 位置错误:确认 @functools.wraps(func) 是紧贴在内部 def wrapper(...): 上方的,而不是在外层装饰器函数上。
  2. 函数对象问题:确认被装饰的对象确实是函数,如果装饰类方法,通常 wraps 依然有效,但需确保 self 参数处理正确。
# 错误示例:位置放错了
def my_decorator(func):
    @functools.wraps(func) # 错!这里装饰的是 my_decorator 本身
    def inner():
        return func()
    return inner

# 正确示例
def my_decorator(func):
    @functools.wraps(func) # 对!这里装饰的是 wrapper
    def wrapper():
        return func()
    return wrapper

10. 完整代码参考

以下是本文涉及的完整可运行代码:

import functools
import time

# 1. 原始函数
def add(a: int, b: int) -> int:
    """
    计算两个整数的和。
    """
    return a + b

# 2. 未使用 @wraps 的装饰器
def simple_timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 执行耗时: {end - start:.4f} 秒")
        return result
    return wrapper

@simple_timer
def subtract(a: int, b: int) -> int:
    """计算两个整数的差。"""
    return a - b

# 3. 使用 @wraps 的装饰器
def smart_timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 执行耗时: {end - start:.4f} 秒")
        return result
    return wrapper

@smart_timer
def multiply(a: int, b: int) -> int:
    """计算两个整数的积。"""
    return a * b

# 4. 带参数的装饰器
def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper_repeat
    return decorator_repeat

@repeat(num_times=2)
def greet(name):
    """向某人打招呼"""
    print(f"Hello, {name}!")

if __name__ == "__main__":
    print("--- 原始函数 ---")
    print(f"Name: {add.__name__}, Doc: {add.__doc__}")

    print("\n--- 未使用 @wraps ---")
    # 这里的 subtract 实际上指向 wrapper
    print(f"Name: {subtract.__name__}, Doc: {subtract.__doc__}")

    print("\n--- 使用 @wraps ---")
    print(f"Name: {multiply.__name__}, Doc: {multiply.__doc__}")
    print(f"Original via __wrapped__: {multiply.__wrapped__.__name__}")

    print("\n--- 带参数装饰器 ---")
    print(f"Name: {greet.__name__}, Doc: {greet.__doc__}")
    greet("Bob")

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文