Pyright如何通过partial保留参数类型信息?
来看这个简单示例:
from functools import partial def foo(a: str, b: int, c: float, d: str) -> bool: print(a, b, c, d) return True bar = partial(foo, 'a', 1, 2.0) bar() # error
Pyright可以正确识别bar还需要一个类型为str的参数d,但它是如何实现的?我查看了Python 3.8-3.10中partial的实际定义以及typeshed中的类型定义。实际定义当然没有类型标注,但typeshed中partial的定义如下:
class partial(Generic[_T]): @property def func(self) -> Callable[..., _T]: ... @property def args(self) -> tuple[Any, ...]: ... @property def keywords(self) -> dict[str, Any]: ... def __new__(cls, __func: Callable[..., _T], *args: Any, **kwargs: Any) -> Self: ... def __call__(__self, *args: Any, **kwargs: Any) -> _T: ... if sys.version_info >= (3, 9): def __class_getitem__(cls, item: Any) -> GenericAlias: ...
该定义中没有任何保留类型信息的逻辑,因为它仅用Any和...来表示参数。你甚至可以复制该定义并将其重命名为mypartial,使用后会发现无法获得相同的类型安全性。我了解可以通过ParamSpec保留类型信息,但此处并未使用,且我的Python版本也无法访问它。
是我遗漏了什么内容,还是Pyright直接硬编码了相关规则,不受类型定义的限制?
答案是Pyright确实对functools.partial做了特殊的硬编码处理,并没有完全依赖typeshed中的类型定义。
类型检查器为了支持这类常见的标准库工具,会针对部分内置/标准库函数添加专门的类型推断逻辑,而非仅依赖表面的类型标注。对于partial,Pyright会在分析代码时,追踪原函数的签名,结合传入的固定参数和关键字参数,动态计算出返回的可调用对象的剩余参数类型。
typeshed里的定义是兼容性的“兜底”版本,用Any和Callable[..., _T]保证在不支持复杂类型推断的检查器中也能正常工作,但Pyright这类进阶工具会绕过这个通用定义,直接实现针对partial的精确类型推导。
你复制定义成mypartial后无法获得同样效果,就是因为Pyright没有为这个自定义类添加特殊处理逻辑,只能按照普通泛型类解析,自然无法保留原函数的参数信息。
内容的提问来源于stack exchange,提问作者Anthony Naddeo

