真实对比两版装饰器——Callable[..., R]和ParamSpec的Callable[P, R],验证同样的参数类型错误在被装饰之后能不能被mypy真实抓出来。
代码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") # 同样的错误
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在类型层面已经变成了"参数任意"的函数。
每一步都是本文上面用真实的mypy 2.3.1跑出来的原始结果。
mypy 2.3.1(venv中安装)typing.ParamSpec(PEP 612)