Python 如何让包装函数复用已有函数的类型签名?
即使 log 已经定义,也可以通过泛型包装工厂,让类型检查器从中推导参数和返回值类型。
Python 标准类型系统没有 log.args、log.kwargs 或 log.return 这样的签名提取语法。原示例还固定传入了 level,这会修改对外签名,需要根据 level 的参数位置分别处理。
完整转发参数:使用 ParamSpec
将已有函数传给泛型工厂,可以建立原函数和包装函数之间的类型关系。P 表示参数签名,R 表示返回值类型。这是 Python typing 文档介绍的典型用途。
下面使用 Python 3.10+ 的导入方式:
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def make_wrapper(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return func(*args, **kwargs)
return wrapper
# log 是已经定义且具有类型标注的函数
wrapped_log = make_wrapper(log)
这里不需要修改 log。类型检查器处理 make_wrapper(log) 时,会根据 log 的类型推导 P 和 R,使 wrapped_log 接受相应的参数。
这个例子只做完整转发,没有固定 level。P.args 和 P.kwargs 必须来自具有绑定关系的 ParamSpec。如果只在模块中声明一个 P,再把它放进独立的 debug 定义中,它不会自动关联到 log。
固定首个位置参数:使用 Concatenate
如果 level 是第一个参数,并且允许按位置传入,可以用 Concatenate 表示固定该参数后剩余的签名。
下面是独立的演示代码,log 的签名仅用于说明适用条件:
from typing import Callable, Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")
def bind_first(
func: Callable[Concatenate[T, P], R],
value: T,
) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return func(value, *args, **kwargs)
return wrapper
def log(
level: str,
message: str,
*,
verbose: bool = False,
) -> None:
print(level, message, verbose)
debug = bind_first(log, "DEBUG")
debug("连接成功", verbose=True)
此时 debug 对外只接收 message 和 verbose,level 由包装函数提供。Concatenate 支持在参数签名前拼接位置参数,不能按参数名称删除任意参数。相关限制见 typing 的 ParamSpec 规范。
固定关键字参数:显式声明剩余签名
如果真实签名类似:
def log(message: str, *, level: str) -> None:
...
那么 level 是 keyword-only 参数,不能套用前面的 bind_first。标准 ParamSpec 和 Concatenate 没有通用的“从签名中移除名为 level 的关键字参数”操作。PEP 612 解释了关键字参数拼接涉及的名称冲突问题。
对于这个假设签名,可以直接写出剩余参数:
def debug(message: str) -> None:
return log(message, level="DEBUG")
如果实际 log 还有其他参数、默认值或重载,需要据此补全包装函数的标注,也可以在 .pyi stub 中维护对外签名。题目没有给出真实签名,因此无法准确列出这些参数。
原示例还需要注意什么
log(level=debug, ...)中的debug在调用时通常指向包装函数本身。它可以作为占位表达,实际代码应传入log要求的日志级别值。- 固定传入
level后,应明确调用者是否还能提供它。如果kwargs中也包含level,调用会因重复提供同一个关键字而失败;位置参数也可能造成重复绑定。 - 要保留原函数的返回值,包装函数必须写
return log(...)。原示例只有调用语句,执行完成后返回的是None。 inspect.signature和修改__annotations__不能替代静态类型声明。functools.wraps可以保留函数元数据,但也不能表达“删除 level 参数”的类型关系。
完全转发参数时,可以使用 ParamSpec 工厂;固定首个位置参数时,可以使用 Concatenate;固定指定关键字参数时,则需要按照真实接口显式声明剩余签名。
备注:内容仅供参考。
