typing.ParamSpec 如何保留被装饰函数的签名参数类型

能,ParamSpec 需配合 Callable 和泛型绑定使用;单独使用仅捕获参数结构,不保留类型信息;IDE 显示 (*args, **kwargs) 是因返回类型未正确声明 P;关键点为显式声明 P、用 Callable[P, R] 约束、wrapper 中必须注解为 *args: P.args 和 **kwargs: P.kwargs。

ParamSpec 能不能保留原函数的参数类型?

能,但必须配合 Callable 和泛型绑定使用,单独用 ParamSpec 不会自动推导参数类型 —— 它只捕获参数结构(数量、名称、是否可变),不携带类型注解信息。

为什么 @decorator 后 IDE 显示参数变成 (*args, **kwargs)?

因为装饰器返回类型没写对。常见错误是把返回类型硬写成 Callable[..., ReturnType],或者漏掉 P 绑定:

  • 错: def my_dec(f: Callable) -> Callable: ... → 彻底丢弃所有签名
  • 错: def my_dec(f: Callable[P, R]) -> Callable[P, R]: ... → 缺少泛型参数声明,P 未定义
  • 对: def my_dec(f: Callable[P, R]) -> Callable[P, R]: ...,且前面声明 P = ParamSpec('P')

完整可工作的 ParamSpec 签名保留写法

关键点有三个:显式声明 P、用 Callable[P, R] 约束输入输出、装饰器内部调用必须保持原调用形式(不能改写成 f(*args, **kwargs) 以外的形式):

from typing import Callable, ParamSpec, TypeVar

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

def log_calls(f: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"Calling {f.__name__}")
        return f(*args, **kwargs)
    return wrapper

@log_calls
def greet(name: str, age: int) -> str:
    return f"Hello {name}, {age} years old"

此时 greet 在 IDE 中鼠标悬停仍显示 (name: str, age: int) -> str,类型检查器也能正确校验传参。

容易被忽略的坑:wrapper 参数注解必须用 P.args / P.kwargs

如果写成 def wrapper(*args, **kwargs)def wrapper(*args: Any, **kwargs: Any),类型系统就断连了 —— P 的结构信息只在 *args: P.args**kwargs: P.kwargs 这种显式绑定时才传递下去:

  • *args: P.args 告诉类型检查器:“这些位置参数的类型和顺序,跟原函数一致”
  • **kwargs: P.kwargs 对应关键字参数的键名与类型约束
  • 漏掉任一,mypy/pyright 就退回 An

    y
    object,签名即失效

ParamSpec 的“保留签名”本质是类型系统里的结构投影,不是运行时魔法 —— 写错一个注解,整条链就断了。