PAYATHON 2026

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;固定指定关键字参数时,则需要按照真实接口显式声明剩余签名。

备注:内容仅供参考。