使用描述符实现typing.Protocol时mypy报签名不兼容是何原因?
问题原因
这是mypy 0.961及更早稳定版本的固有功能缺陷,不是你的描述符实现存在遗漏。
- mypy对
Protocol中以def形式定义的方法成员有特殊的硬编码校验逻辑:默认仅原生Python函数、内置的classmethod/staticmethod/property等官方实现的描述符,可以被正确识别为匹配方法签名的成员。 - 你实现的
UnboundField/BoundField的__get__重载逻辑本身完全符合描述符规范,你给出的普通描述符测试用例可以正常通过校验,也证明通用描述符解析逻辑本身是工作的。但当校验协议中def定义的方法时,mypy不会触发通用描述符解析流程,不会去判断实例访问属性时返回的BoundField是否匹配方法签名,而是直接拿类属性的静态类型UnboundField和协议里的函数签名做结构比对,自然判定签名不兼容。 - 你提到不能直接用
cast转成普通Callable的顾虑完全合理:强转后会丢失自定义方法的类型信息,后续访问自定义方法时mypy会报属性不存在的错误。
可行解决方案
你可以通过TYPE_CHECKING分支做类型层面的兼容,既不改变运行时逻辑,保留自定义方法的类型提示,又能通过协议校验:
from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, Type, TypeVar, Union, overload from typing_extensions import Concatenate, ParamSpec P = ParamSpec("P") R = TypeVar("R") class BoundField(Generic[P, R]): def __init__( self, func: Callable[Concatenate[Any, P], R], instance: Any ) -> None: self.func = func self.instance = instance def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: return self.func(self.instance, *args, **kwargs) class UnboundField(Generic[P, R]): def __init__(self, func: Callable[Concatenate[Any, P], R]) -> None: self.func = func @overload def __get__( self, instance: None, owner: Optional[Type] ) -> "UnboundField[P, R]": ... @overload def __get__( self, instance: object, owner: Optional[Type] ) -> BoundField[P, R]: ... def __get__( self, instance: Optional[object], owner: Optional[Type] = None ) -> Union["UnboundField[P, R]", BoundField[P, R]]: if instance is None: return self return BoundField(func=self.func, instance=instance) def __call__(self, instance: Any, *args: P.args, **kwargs: P.kwargs) -> R: return self.func(instance, *args, **kwargs) # 针对类型检查做兼容 if TYPE_CHECKING: from typing_extensions import Protocol # 定义交叉类型:既是UnboundField(保留自定义方法),又匹配对应方法签名 class _MethodField(UnboundField[P, R], Callable[Concatenate[Any, P], R]): pass def decorator(func: Callable[Concatenate[Any, P], R]) -> _MethodField[P, R]: ... else: def decorator(func: Callable[Concatenate[Any, P], R]) -> UnboundField[P, R]: return UnboundField(func)
这种实现方式下:
- 运行时逻辑和你原来的代码完全一致,没有额外开销
- 类型检查时,mypy会识别到decorator返回的对象既带有UnboundField/BoundField的所有自定义方法,又符合协议要求的方法签名,不会报不兼容错误
- 访问自定义方法时也能获得正确的类型提示
你也可以通过一个简单的测试验证问题根源:把协议I里的方法定义从def形式改成显式可调用类型注解:
from typing import Callable class I(Protocol): m: Callable[[int], int]
此时mypy会走通用描述符解析逻辑,正确识别B().m是接受int返回int的可调用对象,不会抛出签名不兼容的错误,这也直接证明问题出在mypy对def形式协议方法的特殊校验逻辑上。
该缺陷目前已经被mypy维护团队确认,后续版本会优化协议方法的校验逻辑,支持自定义描述符实现的方法类成员。
内容的提问来源于stack exchange,提问作者sagittarian
相关产品推荐
相关产品推荐

