能,ParamSpec 需配合 Callable 和泛型绑定使用;单独使用仅捕获参数结构,不保留类型信息;IDE 显示 (*args, **kwargs) 是因返回类型未正确声明 P;关键点为显式声明 P、用 Callable[P, R] 约束、wrapper 中必须注解为 *args: P.args 和 **kwargs: P.kwargs。
能,但必须配合 Callable 和泛型绑定使用,单独用 ParamSpec 不会自动推导参数类型 —— 它只捕获参数结构(数量、名称、是否可变),不携带类型注解信息。
因为装饰器返回类型没写对。常见错误是把返回类型硬写成 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')
关键点有三个:显式声明 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,类型检查器也能正确校验传参。
如果写成 def wrapper(*args, **kwargs) 或 def wrapper(*args: Any, **kwargs: Any),类型系统就断连了 —— P

*args: P.args 和 **kwargs: P.kwargs 这种显式绑定时才传递下去:
*args: P.args 告诉类型检查器:“这些位置参数的类型和顺序,跟原函数一致”**kwargs: P.kwargs 对应关键字参数的键名与类型约束Any 或 object,签名即失效ParamSpec 的“保留签名”本质是类型系统里的结构投影,不是运行时魔法 —— 写错一个注解,整条链就断了。