MyPy:虚拟类继承的“不兼容类型”

Suk*_*mbu 6 python type-hinting mypy

演示代码

#!/usr/bin/env python3

from abc import ABCMeta, abstractmethod

class Base(metaclass = ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        return (
            hasattr(subclass, 'x')
        )

    @property
    @abstractmethod
    def x(self) -> float:
        raise NotImplementedError

class Concrete:
    x: float = 1.0

class Application:
    def __init__(self, obj: Base) -> None:
        print(obj.x)

ob = Concrete() 
app = Application(ob)

print(issubclass(Concrete, Base))
print(isinstance(Concrete, Base))
print(type(ob))
print(Concrete.__mro__)
Run Code Online (Sandbox Code Playgroud)

python test_typing.py返回:

1.0
True
False
<class '__main__.Concrete'>
(<class '__main__.Concrete'>, <class 'object'>)
Run Code Online (Sandbox Code Playgroud)

mypy test_typing.py返回:

test_typing.py:30: error: Argument 1 to "Application" has incompatible type "Concrete"; expected "Base"
Found 1 error in 1 file (checked 1 source
Run Code Online (Sandbox Code Playgroud)

但如果我将行更改class Concrete:class Concrete(Base):,我会得到 python test_typing.py以下结果:

1.0
True
False
<class '__main__.Concrete'>
(<class '__main__.Concrete'>, <class '__main__.Base'>, <class 'object'>)
Run Code Online (Sandbox Code Playgroud)

为此mypy test_typing.py

Success: no issues found in 1 source file
Run Code Online (Sandbox Code Playgroud)

如果我将以下代码添加到我的代码中:

reveal_type(Concrete)
reveal_type(Base)
Run Code Online (Sandbox Code Playgroud)

我在这两种情况下都得到相同的结果mypy test_typing.py

test_typing.py:37: note: Revealed type is "def () -> vmc.test_typing.Concrete"
test_typing.py:38: note: Revealed type is "def () -> vmc.test_typing.Base"
Run Code Online (Sandbox Code Playgroud)

结论

似乎很明显,MyPi 在虚拟基类方面存在一些问题,但非虚拟继承似乎按预期工作。

问题

在这些情况下 MyPy 的类型估计如何工作?有解决方法吗?

第二个演示代码

使用Protocol模式:

1.0
True
False
<class '__main__.Concrete'>
(<class '__main__.Concrete'>, <class 'object'>)
Run Code Online (Sandbox Code Playgroud)

优点

  • mypy:Success: no issues found in 1 source file
  • isinstance(Concrete, Base):True

缺点

  • 不使用issubclass(Concrete, Base)TypeError: Protocols with non-method members don't support issubclass()
  • 不检查__init__方法签名:__init__(self, x: float) -> Nonevs. __init__(self) -> None(为什么在这里返回inspect.signature()字符串(self, *args, **kwargs)and (self, /, *args, **kwargs)?用class Base:而不是class Base(Protocol):i get (self, x: float) -> Noneand (self, /, *args, **kwargs)
  • @abstractmethod忽略和之间的区别@classmethod(将任何方法视为抽象)

第三个演示代码

这次只是第一个代码的更复杂的示例:

test_typing.py:30: error: Argument 1 to "Application" has incompatible type "Concrete"; expected "Base"
Found 1 error in 1 file (checked 1 source
Run Code Online (Sandbox Code Playgroud)

优点

  • issubclass(Concrete, Base):True
  • isinstance(Concrete, Base):False
  • 方法签名检查也适用于__init__.

缺点

  • 不使用 MyPy:
    test_typing.py:42: error: Argument 1 to "Application" has incompatible type "Concrete"; expected "Base"
    Found 1 error in 1 file (checked 1 source file)
    
    Run Code Online (Sandbox Code Playgroud)

第四个演示代码

在某些情况下,以下代码可能是一个可能的解决方案。

1.0
True
False
<class '__main__.Concrete'>
(<class '__main__.Concrete'>, <class '__main__.Base'>, <class 'object'>)
Run Code Online (Sandbox Code Playgroud)

优点:

  • __init__由于使用而自动生成的方法@dataclass。这里:(self, x: float = 0.0, y: float = 0.0, z: float = 0.0, w: float = 1.0) -> None
  • 适用于isinstance()True
  • 与 mypy 一起使用:Success: no issues found in 1 source file

缺点:

  • 您需要希望下一个开发人员@dataclass在实现您的界面时使用它。
  • 不适用于__init__不仅采用类属性的方法。

提示:如果不需要强制__init__方法并且只想处理属性,那么只需省略@dataclass

第五个演示代码

更新了第四个代码以提供更多安全性,但没有隐式__init__方法:

Success: no issues found in 1 source file
Run Code Online (Sandbox Code Playgroud)

目前的结论

Protocol不稳。但这种Metaclass方式是不可检查的,因为它不能与 MyPy 一起使用(因为它不是静态的)。

更新的问题

是否有任何替代解决方案来实现某种类型的接口(不带class Concrete(Base))并使其类型安全(可检查)?

Suk*_*mbu 0

结果

经过一些测试和更多研究后,我确信实际问题是Protocol默默地覆盖定义的__init__方法的行为。

结论

似乎合乎逻辑,因为协议并不是要启动的。但有时需要定义一个__init__方法,因为在我看来,__init__方法也是类及其对象接口的一部分。

解决方案

我发现了一个关于这个问题的现有问题,这似乎证实了我的观点:https ://github.com/python/cpython/issues/88970

幸运的是它已经修复: https://github.com/python/cpython/commit/5f2abae61ec69264b835dcabe2cdabe57b9a990e

但不幸的是,此修复仅适用于 Python 3.11 及更高版本。

目前可用的 Python 3.10.5。

警告:就像问题中提到的那样,某些静态类型检查器在这种情况下可能表现不同。MyPy只是忽略丢失的__init__方法(测试过,确认),但Pyright似乎检测并报告丢失的__init__方法(未经我测试)。