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

How to implement an interface in a way that is compatible with static type checks?

我有两个基础 classes,FooBar,以及一个 Worker class,它期望对象的行为类似于 Foo.然后我添加另一个 class 来实现 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

此处 Worker 实际上接受任何 Foo-ish 对象,即具有属性 x 和方法 foo 的对象。所以如果 obj 走路像 Foo 并且叫声像 Foo 那么 Worker 就会很开心。现在整个项目都使用类型提示,所以目前我指出 obj: Foo。到目前为止一切顺利。

现在有另一个 class FooBar,它是 class 的子 Bar 并且表现得像 Foo 但它不能子class 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

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

error: Argument 1 to "Worker" has incompatible type "FooBar"; expected "Foo"

使用抽象基础class

为了将 Foo-ish 的接口与类型检查器进行通信,我考虑为 Foo-ish 类型创建一个抽象基础 class:

import abc

class Fooish(abc.ABC):
    x : int

    @abc.abstractmethod
    def foo(self) -> int:
        raise NotImplementedError

但是我不能让 FooBar 继承自 Fooish 因为 Bar 有它自己的 metaclass 所以这会导致 metaclass冲突。所以我考虑在 FooFooBar 上使用 Fooish.register 但 mypy 不同意:

@Fooish.register
class Foo:
    ...

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

class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x

产生以下错误:

error: Argument 1 to "Worker" has incompatible type "Foo"; expected "Fooish"
error: Argument 1 to "Worker" has incompatible type "FooBar"; expected "Fooish"

使用 "normal" class 作为接口

我考虑的下一个选项是以 "normal" class 的形式创建一个不从 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

现在 mypy 不会抱怨 Worker.__init__ 的参数类型,但会抱怨 FooBar.xproperty)与 Fooish.x 的签名不兼容:

error: Signature of "x" incompatible with supertype "Fooish"

此外,Fooish(抽象)基础 class 现在是可实例化的,并且是 Worker(...) 的有效参数,尽管它没有意义,因为它不提供属性 x.

问题...

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

如果我明白了,你也许可以添加一个 Union,基本上,允许 Foo or Bar or Fooish:

from typing import Union

class Worker:
    def __init__(self, obj: Union[Bar, Fooish]):
        self.x = obj.x

# no type error
Worker(FooBar())

具有以下内容:

class MyMeta(type):
    pass

class Fooish:
    x: int
    def foo(self) -> int:
        raise NotImplementedError


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: Union[Bar, Fooish]):
        self.x = obj.x


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

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

    def foo(self):
        pass

见:

  1. 要去掉 error: Signature of "x" incompatible with supertype "Fooish" 你可以注释 x: typing.Any.
  2. 要使 Fooish 真正抽象,需要一些技巧来解决元类冲突。我从 this answer:
  3. 那里拿了一份食谱
class MyABCMeta(MyMeta, abc.ABCMeta):
    pass

之后可以创建 Fooish:

class Fooish(metaclass=MyABCMeta):

在运行时成功执行并且没有显示来自 mypy 的错误的整个代码:

import abc
import typing

class MyMeta(type):
    pass

class MyABCMeta(abc.ABCMeta, MyMeta):
    pass

class Fooish(metaclass=MyABCMeta):
    x : typing.Any

    @abc.abstractmethod
    def foo(self) -> int:
        raise NotImplementedError

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

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

    def foo(self):
        pass

class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x


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

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

    def foo(self):
        pass

print(Worker(FooBar()))

现在是时候考虑一​​下您是否真的要将 Fooish 抽象化,因为如果 MyMeta 做了很多技巧,那么 class Fooish(metaclass=MyABCMeta): 可能会产生副作用。例如,如果 MyMeta 定义了 __new__,您可以在 Fooish 中定义 __new__,它不会调用 MyMeta.__new__,但会调用 abc.ABCMeta.__new__。但是事情可能会变得复杂......所以,也许非抽象会更容易 Fooish.

支持 PyPI 上的 structural subtyping was added by PEP 544 -- Protocols: Structural subtyping (static duck typing) starting with Python 3.8. For versions prior to 3.8 the corresponding implementation is made available by the typing-extensions 包。

与所讨论的场景相关的是 typing.Protocol as explained by the PEP in more detail. This allows to define implicit subtypes,这使我们免于元类冲突问题,因为不需要继承。所以代码看起来像这样:

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