Python 黑魔法:合并两个 Python 函数的签名

💡 原文英文,约1200词,阅读约需5分钟。
📝

内容提要

文章介绍如何合并两个Python函数的签名:包装函数new在old基础上增加位置参数,但help显示为new(prefix, foo, *args, **kwargs),信息不足。作者利用functools.wraps和inspect.signature的follow_wrapped机制,通过types.CodeType手工构造代码对象,合并两函数的参数、默认值和注解,生成新的__wrapped__函数,使help显示为new(prefix, foo, bar)。代码已发布到GitHub和PyPI(merge_args)。

🔎

延伸解读

为什么需要合并签名

当包装函数在原有函数基础上增加位置参数时,直接使用functools.wraps会让help显示原函数的签名,丢失新增参数;而手动定义完整签名又需随原函数变化而维护。文章中的方法通过构造新的__wrapped__函数,既保留了包装函数新增的参数,又自动合并了原函数的参数,使help显示为new(prefix, foo, bar),兼顾了准确性与可维护性。

核心机制:follow_wrapped与代码对象

inspect.signature默认follow_wrapped=True,会追踪__wrapped__属性来获取签名。因此,只要为包装函数设置一个具有合并签名的__wrapped__函数,help和inspect就能显示正确签名。文章没有使用eval生成字符串,而是通过types.CodeType手工构造代码对象,合并两个函数的参数、默认值和注解,再结合types.FunctionType创建函数。这要求理解代码对象的argcount、kwonlyargcount和varnames等字段。

实现限制与适用边界

该方法明确假设源函数不使用可变参数(*args或**kwargs),因此合并后的签名由目标函数和源函数的位置参数、关键字参数及仅关键字参数依次拼接而成。此外,构造的__wrapped__函数仅用于让help和inspect等工具正确显示签名,并不预期被直接调用。这意味着它适合作为元数据包装,而非替代实际调用逻辑。若源函数包含可变参数,则需要更复杂的处理。

❓

Q&A

如何让包装函数的 help 显示合并后的签名,而不是 *args, **kwargs?

可以通过设置包装函数的 __wrapped__ 属性为一个具有合并签名的函数。具体做法是使用 types.CodeType 手工构造一个代码对象,合并原函数和包装函数的参数、默认值和注解,然后创建新函数并赋值给包装函数的 __wrapped__。这样 inspect.signature 默认会跟随 __wrapped__,从而显示合并后的签名。

functools.wraps 和 inspect.signature 的 follow_wrapped 参数有什么关系?

functools.wraps 会复制 __module__、__name__、__doc__、__annotations__ 等属性,并设置 __wrapped__ 指向原函数。inspect.signature 有一个 follow_wrapped 参数(默认为 True),当它为 True 时,会使用 __wrapped__ 来获取签名。因此,通过控制 __wrapped__ 可以影响 help 和 inspect 显示的签名。

为什么不能直接用 functools.wraps 来合并两个函数的签名?

因为 functools.wraps 只是复制原函数的属性并设置 __wrapped__,它不会合并两个函数的参数。如果直接使用,包装函数的签名会完全变成原函数的签名,丢失包装函数新增的参数(如 prefix),导致 help 显示不完整。

手工构造代码对象时,需要关注 CodeType 的哪些字段?

主要关注三个字段:argcount(位置参数个数,不包括关键字-only参数、*args和**kwargs)、kwonlyargcount(关键字-only参数个数)和 varnames(参数和局部变量的名称元组)。其他字段可以从现有函数复制或使用安全默认值。

merge_args 支持哪些类型的参数合并?有哪些限制?

merge_args 支持合并位置参数、带默认值的参数(keyword-maybe)和关键字-only参数。但它有一个限制:不支持源函数使用可变参数(*args 或 **kwargs)。合并后的签名顺序为:目标函数的位置参数、源函数的位置参数、目标函数的带默认值参数、源函数的带默认值参数、目标函数的关键字-only参数、源函数的关键字-only参数。

merge_args 的代码在哪里可以获取?如何安装?

代码已在 GitHub 上发布,也可以通过 PyPI 安装:pip install merge_args。项目还包含一个广泛的测试套件。

🏷️

标签

➡️

继续阅读