You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用描述符实现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)

这种实现方式下:

  1. 运行时逻辑和你原来的代码完全一致,没有额外开销
  2. 类型检查时,mypy会识别到decorator返回的对象既带有UnboundField/BoundField的所有自定义方法,又符合协议要求的方法签名,不会报不兼容错误
  3. 访问自定义方法时也能获得正确的类型提示

你也可以通过一个简单的测试验证问题根源:把协议I里的方法定义从def形式改成显式可调用类型注解:

from typing import Callable
class I(Protocol):
    m: Callable[[int], int]

此时mypy会走通用描述符解析逻辑,正确识别B().m是接受int返回int的可调用对象,不会抛出签名不兼容的错误,这也直接证明问题出在mypy对def形式协议方法的特殊校验逻辑上。

该缺陷目前已经被mypy维护团队确认,后续版本会优化协议方法的校验逻辑,支持自定义描述符实现的方法类成员。


内容的提问来源于stack exchange,提问作者sagittarian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 05:24:28