Python functools.wraps:装饰之后,函数名字和签名为什么仍值得保留

10-01 3阅读

功能正常,工具却认错了函数

给数据处理函数套上一层计数装饰器以后,返回值没有变化,帮助信息里却只剩下包装函数的名字,自动生成的参数说明也成了一对星号参数。这个问题不一定会让业务立刻失败,却会使日志定位、接口文档和调试工具失去原来可以利用的信息。

装饰器通常返回一个新函数,外部名称因此指向包装层。原函数的名字和说明不会因为包装层调用过它就自动继承。functools.wraps 可以在定义包装函数时补上常用元数据,并留下通向被包装对象的引用,方便工具理解这层关系。

用同一个原函数比较两种包装

下面代码只使用标准库,保存为 Python 文件运行,本文在三点十二验证。两种装饰器都正常转发参数,只有其中一种使用 wraps。这样可以把“调用结果相同”和“工具看到的信息相同”分开检查,避免把元数据问题误诊成计算问题。

原函数特意把次数设为只能通过关键字传入。检查既要看到原来的签名,也要确认错误的位置参数仍然被拒绝。计数器只用来显示确实经过了包装层,不用于并发统计,也不会在示例中访问网络或修改外部资源。

Python functools.wraps:装饰之后,函数名字和签名为什么仍值得保留

AI概念配图,非真实界面

from functools import wraps
import inspect

def bare(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def counted(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

def repeat(text, *, times=2):
    "Repeat a text value."
    return text * times

plain = bare(repeat)
safe = counted(repeat)
print('names:', plain.__name__, safe.__name__)
print('signature:', inspect.signature(safe))
print('outer signature:', inspect.signature(safe, follow_wrapped=False))
print('result:', safe('go', times=3))
assert safe.__doc__ == repeat.__doc__
assert safe.__wrapped__ is repeat
assert inspect.unwrap(safe) is repeat
assert safe.calls == 1
assert safe.__wrapped__('x', times=2) == 'xx'
assert safe.calls == 1
try:
    safe('go', 3)
except TypeError:
    print('positional times: rejected')
else:
    raise AssertionError('keyword-only boundary lost')
print('all wraps checks passed')

签名看起来没变,不代表包装函数被改写

输出中的两个名称分别是 wrapper 和 repeat;默认检查得到的签名保留了关键字限制,关闭跟随后则显示外层实际声明的参数。wraps 没有重新生成函数体,也没有把包装函数的参数列表改写成原函数的声明,它让支持该约定的工具能够沿引用继续查看。

真正的参数约束仍由调用链决定。示例外层接住任意参数,再原样交给原函数,所以不合法的位置参数在内部调用时触发异常。如果包装层吞掉参数、替换返回值或者捕获所有异常,添加 wraps 并不能把这些行为恢复正确,仍需独立验收。

这里的计数是调用尝试次数,因此失败调用也会先加一。若要统计成功次数,应把增加动作放在原函数成功返回之后。元数据装饰与业务计数是两套规则,最好先写清统计口径,再决定包装层里每个动作的先后顺序。

原函数引用不是权限边界

直接调用 __wrapped__ 会绕过当前计数层,例子用断言验证这一点。这个入口适合测试和检查,但不能把包装器当成防止调用底层函数的安全屏障。真正涉及权限的规则应在可信执行边界上完成,不能依赖别人不知道某个属性名。

多个装饰器叠加时,每一层都应保留自己的包装关系。inspect.unwrap 可以沿完整链找到最内层对象;其中某层没有维护引用,工具可能只能停在那里。排查时逐层检查名称、说明和签名,通常比只看最外层调用结果更容易发现遗漏。

复制的说明也有适用范围。如果装饰器有意新增参数或改变接口语义,原说明可能已经不准确,应为新接口补充文档和测试。不要为了让自动生成的页面看起来整齐,隐藏调用者实际需要知道的额外参数、限制或副作用。

迁移现有装饰器时,可以先选一个常用函数,保存名字、帮助文本、签名及正常和异常调用的结果,再加入 wraps 对照。预期变化应集中在可观察的元数据上;若行为也变了,就继续检查参数转发和返回路径,而不是把变化全部归因于这个辅助工具。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。