typing.ParamSpec 如何保留被装饰函数的 *args / **kwargs 类型

10次阅读

不能——ParamSpec仅记录参数结构“形状”,不保存args/kwargs的具体类型注解,P.args恒为tuple[Object, …],需用Concatenate显式拼接才能保留如args: Float等类型信息。

typing.ParamSpec 如何保留被装饰函数的 *args / **kwargs 类型

ParamSpec 能不能原样保留 *args**kwargs 的类型?

不能直接保留——ParamSpec 本身不捕获 *args: P.args**kwargs: P.kwargs 的具体类型,它只记录参数结构的“形状”,不保存动态参数的实际注解。如果你写 def f(*args: int, **kwargs: str)P = ParamSpec('P') 绑定后,P.argstuple[object, ...]P.kwargsdict[str, Any],原始 intstr 信息就丢了。

Concatenate + 显式标注才能保留 *args 类型

要让装饰器把 *args: int 传下去,必须手动拆开参数结构,用 Concatenate 把固定参数和可变参数拼起来,并显式写出 *args 的类型。常见错误是只写 P,结果类型检查器认为 *args泛型占位符而非具体类型。

  • ParamSpec 适合转发签名但不关心 *args/**kwargs 具体类型(比如日志装饰器)
  • 若需保留 *args: int,定义装饰器时得用 Callable[Concatenate[int, P], R],并让被装饰函数显式标注 *args: int
  • **kwargs 同理:用 Concatenate[Unpack[T], P]python 3.12+)或配合 TypedDict 模拟强类型 **kwargs

实际例子:带类型感知的重试装饰器

下面这个装饰器能正确推导 f(x: str, *args: float, **kwargs: bool)*argsfloat**kwargsbool

from typing import Callable, TypeVar, ParamSpec, Concatenate, Unpack, TypedDict import time 

P = ParamSpec('P') R = TypeVar('R')

假设我们只关心 *args: float,其他保持原样

def retry( func: Callable[Concatenate[float, P], R] ) -> Callable[Concatenate[float, P], R]: def wrapper(*args: float, *kwargs: P.kwargs) -> R: for _ in range(3): try: return func(args, **kwargs) except Exception: time.sleep(1) raise RuntimeError("Failed after retries") return wrapper

使用时必须显式标注 *args / **kwargs 类型

def my_func(x: str, *args: float, **kwargs: bool) -> int: return len(x) + sum(int(a) for a in args)

wrapped = retry(my_func) # ✅ mypy 知道 wrapped 接收 *args: float, **kwargs: bool

为什么 P.args 总是 tuple[object, ...]

这是 ParamSpec 的设计限制:它抽象的是“调用时参数如何分组”,不是“每个参数的静态类型”。P.args 对应的是 *args 形参整体,而 Python 类型系统中 *args: T 的类型本质是 tuple[T, ...],但 P 不存储这个 T——它只存 tuple[object, ...] 作为占位。真正要恢复 T,只能靠 Concatenate 显式拼接,或用 Callable[[int, str, *tuple[float, ...]], None] 这种硬编码方式。

所以别指望 ParamSpec 自动推导出 *args 的元素类型;它最常被误用的地方,就是以为 P.args 能当 tuple[float, ...] 用。

text=ZqhQzanResources