bigywater.tech
Python · 53

ParamSpec:装饰器如何保留原函数参数类型

真实对比两版装饰器——Callable[..., R]和ParamSpec的Callable[P, R],验证同样的参数类型错误在被装饰之后能不能被mypy真实抓出来。

Callable[..., R]装饰器
真实丢失参数类型,错误调用检查不出来
ParamSpec装饰器
真实完整保留参数签名,同样的错误能被抓出来
引入背景
真实PEP 612专门修复装饰器丢失类型这个漏洞

做的事:两版装饰器,一个用...一个用ParamSpec

代码P = ParamSpec("P") R = TypeVar("R") def logged_bad(fn: Callable[..., R]) -> Callable[..., R]: def wrapper(*args, **kwargs): return fn(*args, **kwargs) return wrapper def logged_good(fn: Callable[P, R]) -> Callable[P, R]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: return fn(*args, **kwargs) return wrapper @logged_bad def add_bad(a: int, b: int) -> int: return a + b @logged_good def add_good(a: int, b: int) -> int: return a + b add_bad(1, "oops") # 第二个参数类型错了 add_good(1, "oops") # 同样的错误

验证:Callable[..., R]装饰过的函数,真实丢失了参数类型检查

实测 · 本机执行输出(mypy 2.3.1)
paramspec_test.py:28: error: Argument 2 to "add_good" has incompatible type "str";
expected "int"  [arg-type]
Found 1 error in 1 file

真实结果:add_bad(1, "oops")这一行完全没有出现在错误列表里!

这是Callable[..., R]这种写法真实的代价:...在Callable类型标注里的含义是"参数类型未知/任意",logged_bad的签名真实告诉mypy"不管传进来的fn参数类型是什么,包装后返回的函数也接受任意参数"——原始函数add_bad明明真实标注了a: int, b: int,但一旦被logged_bad装饰,这份类型信息真实在装饰的那一步就丢失了,add_bad(1, "oops")这种真实错误的调用,mypy完全没有能力抓出来,因为装饰后的add_bad在类型层面已经变成了"参数任意"的函数。

验证:ParamSpec真实把原函数的参数签名完整搬运到了包装函数上

对比之下,logged_good用Callable[P, R]标注,其中P是一个ParamSpec——它真实捕获的不是某一个具体类型,而是"一整套参数签名"(有几个位置参数、什么类型、有没有关键字参数),wrapper内部用*args: P.args, **kwargs: P.kwargs真实把这套完整的参数签名重新应用在包装函数自己身上。结果是add_good被logged_good装饰之后,在mypy看来它的参数签名跟原始未被装饰的add_good函数真实是完全一样的(a: int, b: int),add_good(1, "oops")这次真实被mypy正确地抓出来,报的错误信息里甚至真实精确指出了"第二个参数"和期望的类型int。这解释了ParamSpec存在的真实意义:在Python里写一个通用的装饰器,本质上是在做"接受一个函数、返回一个新函数"这件事,如果装饰器本身的类型标注写得不够精确(比如偷懒用...),被装饰函数原本精心写好的参数类型标注会在装饰这一步真实全部作废——这也是很多真实项目里"加了类型标注,但用了装饰器之后类型检查突然形同虚设"这类问题背后常见的根本原因,ParamSpec正是Python 3.10(PEP 612)专门为了修复这个真实漏洞引入的。

分步演示:Callable[...,R]和ParamSpec装饰同一个函数的真实对比

每一步都是本文上面用真实的mypy 2.3.1跑出来的原始结果。

同一个类型错误,两种装饰器写法下mypy的真实反应完全不同
这一步新验证的装饰器

    本文用到的工具

    • 本机 Python 3.14.7,真实的 mypy 2.3.1(venv中安装)
    • 标准库 typing.ParamSpec(PEP 612)
    京ICP备2026067502号-1京公网安备11010502063487号