如何以与静态类型检查兼容的方式实现接口?

a_g*_*est 5 python static-typing interface python-3.x mypy

我有两个基本类FooBar,以及一个Worker期望行为类似的类Foo。然后,我添加了另一个实现所有相关属性和方法的类,Foo但是我没有设法通过mypy将其成功传递给静态类型检查。这是一个小例子:

class MyMeta(type):
    pass

class Bar(metaclass=MyMeta):
    def bar(self):
        pass

class Foo:
    def __init__(self, x: int):
        self.x = x

    def foo(self):
        pass

class Worker:
    def __init__(self, obj: Foo):
        self.x = obj.x
Run Code Online (Sandbox Code Playgroud)

这里Worker实际上接受任何Fooish对象,即具有属性x和方法的对象foo。因此,如果obj走路像a一样Foo,如果它像a一样嘎嘎作响,Foo那么Worker它将很高兴。现在,整个项目都使用类型提示,因此暂时指示obj: Foo。到目前为止,一切都很好。

现在有另一个类FooBar,该类具有子类Bar和行为,Foo但不能子类化,Foo因为它通过属性公开其属性(因此__init__参数没有意义):

class FooBar(Bar):
    """Objects of this type are bar and they are foo-ish."""

    @property
    def x(self) -> int:
        return 0

    def foo(self):
        pass
Run Code Online (Sandbox Code Playgroud)

在这一点上,Worker(FooBar())显然会导致类型检查器错误:

error: Argument 1 to "Worker" has incompatible type "FooBar"; expected "Foo"
Run Code Online (Sandbox Code Playgroud)

使用抽象基类

为了将Foo-ish 的接口传达给类型检查器,我考虑过为Foo-ish类型创建抽象基类:

import abc

class Fooish(abc.ABC):
    x : int

    @abc.abstractmethod
    def foo(self) -> int:
        raise NotImplementedError
Run Code Online (Sandbox Code Playgroud)

但是我不能FooBar从它继承,Fooish因为Bar它有自己的元类,因此会引起元类冲突。所以我考虑过Fooish.register在两者上使用FooFooBar但是mypy并不同意:

@Fooish.register
class Foo:
    ...

@Fooish.register
class FooBar(Bar):
    ...

class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x
Run Code Online (Sandbox Code Playgroud)

产生以下错误:

error: Argument 1 to "Worker" has incompatible type "Foo"; expected "Fooish"
error: Argument 1 to "Worker" has incompatible type "FooBar"; expected "Fooish"
Run Code Online (Sandbox Code Playgroud)

使用“普通”类作为接口

我考虑的下一个选项是创建一个接口,而无需abc.ABC以“常规”类的形式继承,然后同时拥有FooFooBar继承它:

class Fooish:
    x : int

    def foo(self) -> int:
        raise NotImplementedError

class Foo(Fooish):
    ...

class FooBar(Bar, Fooish):
    ...

class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x
Run Code Online (Sandbox Code Playgroud)

现在,mypy不再抱怨的参数类型,Worker.__init__而是抱怨FooBar.x(是property)与Fooish.x以下项的签名不兼容:

error: Signature of "x" incompatible with supertype "Fooish"
Run Code Online (Sandbox Code Playgroud)

而且,Fooish(抽象)基类现在可以实例化,并且是有效的参数,Worker(...)尽管它没有提供属性,但没有意义x

问题...

现在,我陷入了一个问题,即如何在不使用继承的情况下将该接口与类型检查器进行通信(由于元类冲突;即使有可能,mypy仍会抱怨的签名不兼容x)。有办法吗?

a_g*_*est 7

PEP 544添加了对结构子类型的支持- 协议:从 Python 3.8 开始,结构子类型(静态鸭子类型) 。对于 3.8 之前的版本,PyPI 上的Typing-extensions包提供了相应的实现。

typing.ProtocolPEP更详细地解释了与所讨论的场景相关的内容。这允许定义隐式子类型,从而使我们免于元类冲突问题,因为不需要继承。所以代码看起来像这样:

from typing import Protocol             # Python 3.8+
from typing_extensions import Protocol  # Python 3.5 - 3.7


class Fooish(Protocol):
    x : int

    def foo(self) -> int:
        raise NotImplementedError


# No inheritance required, implementing the defined protocol implicitly subtypes 'Fooish'.
class Foo:
    def __init__(self, x: int):
        self.x = x

    def foo(self):
        pass


class MyMeta(type):
    pass


class Bar(metaclass=MyMeta):
    def bar(self):
        pass


# Here, we again create an implicit subtype of 'Fooish'.
class FooBar(Bar):
    """Objects of this type are bar and they are foo-ish."""

    @property
    def x(self) -> int:
        return 0

    @x.setter
    def x(self, val):
        pass

    def foo(self):
        pass


class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x
Run Code Online (Sandbox Code Playgroud)