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

Posted

技术标签:

【中文标题】如何以与静态类型检查兼容的方式实现接口?【英文标题】:How to implement an interface in a way that is compatible with static type checks? 【发布时间】:2020-02-09 10:06:43 【问题描述】:

我有两个基类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

这里Worker 实际上接受任何Foo-ish 对象,即具有属性x 和方法foo 的对象。所以如果obj 走路像Foo 并且如果它像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

此时,执行Worker(FooBar()) 显然会导致类型检查器错误:

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

使用抽象基类

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

import abc

class Fooish(abc.ABC):
    x : int

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

但是我不能让FooBarFooish 继承,因为Bar 有自己的元类,所以这会导致元类冲突。所以我考虑在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"

使用“普通”类作为接口

我考虑的下一个选项是以“普通”类的形式创建一个不从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.x(即 property)与 Fooish.x 的签名不兼容:

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

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

问题...

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

【问题讨论】:

【参考方案1】:

从 Python 3.8 开始,PEP 544 -- Protocols: Structural subtyping (static duck typing) 添加了对 structural subtyping 的支持。对于 3.8 之前的版本,PyPI 上的 typing-extensions 包提供了相应的实现。

与讨论的场景相关的是typing.Protocol,正如 PEP in more detail 所解释的那样。这允许定义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

【讨论】:

这个不错!当我第一次看到你的问题时,我想到了协议(之前听说过它们)但我没有对它们进行任何练习,所以没有使用它们并写了一些东西来绕过元类冲突:) 但它们确实最适合你案子。从您的回答中学到了一些新东西,谢谢! 此外,协议的使用解决了 Fooish 中的 x: int 和子类中的属性的问题,以前使用 mypy 提供了 error: Signature of "x" incompatible with supertype "Fooish" @sanyash 实际上,通过在FooBar 中添加@x.setter 修复了不兼容错误(我在上面的代码示例中这样做了)。删除 setter 并执行 Worker(FooBar()) 会出现不兼容错误以及以下注释:“Protocol member Fooish.x expected settable variable, got read-only attribute”. 如果我在 Fooish 中将我的元类代码与 x: int 以及在 FooBar 中的 x.getterx.setter 一起使用,我会得到 error: Signature of "x" incompatible with supertype "Fooish"。所以 setter 在这种情况下似乎没有帮助。 @sanyash 实际上,我刚刚查看了您链接的其他问题的答案,它指的是bug in mypy。所以一般来说它应该通过添加setter来工作。【参考方案2】:

如果我理解,您也许可以添加一个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

见:

https://github.com/python/typing/issues/213

【讨论】:

我也考虑过使用Union,而不是Fooish = Union[Foo, FooBar]。然而,这有一个问题,即在解析模块时需要知道所有可能的类型。由于包作为一个框架,开发人员应该创建自己的 Foo-ish 类型,但是注册它们以便他们可以使用 Worker 是不可能的(或者是这样吗?也许需要对模块进行一些重构和具体的进口订单?)。因此,用户会收到类型警告。我很快检查了您链接的问题,这确实看起来有帮助,所以也许我必须等待它。 进行更多研究后,我发现 PEP 544 添加了对允许隐式子类型(不需要继承)的结构子类型的支持。如果您有兴趣,我将其总结在一个单独的answer 中。【参考方案3】:
    要删除error: Signature of "x" incompatible with supertype "Fooish",您可以注释x: typing.Any。 要使Fooish 真正抽象化,需要一些技巧来解决元类冲突。我从this answer那里拿了一份食谱:
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会更容易。

【讨论】:

在界面中使用x: Any 会破坏此处类型提示的使用。这些属性的类型对于Worker 很重要,我希望对它们进行类型检查,因此任何实现Fooish 的类都必须提供x: int。使用x: Any 既不会将其传达给类型检查器,也不会传达给开发人员。我同意将 ABC 与继承一起使用会变得一团糟,尤其是由于 ABC 混合。这就是我尝试使用ABC.register 的原因。但是使用“普通”类作为接口 mixin 会使模块具有可实例化的 mixin 类型,甚至可以通过 Worker(...) 的类型检查。 @a_guest 我同意使用 x: typing.Any 会浪费 x 是 int 的信息。询问有关它的单独问题:***.com/questions/58349417/…. @a_guest 可能将Fooish 实现为普通类,但使用def __init__(self, *args, **kwargs): raise RuntimeError('Cant instanciate Fooish') 可以吗? @a_guest 在这里得到了一个很好的答案:***.com/a/58349527/9609843。 TLDR:在 Fooish 而不是 x: int 中实现一个带有 getter 和 setter 的属性(可能是抽象的)。 确实,感谢您的指点。通过尝试使示例代码尽可能紧凑,我完全错过了 w.r.t 的不对称性。获取/设置属性x。做更多研究后,我发现 PEP 544 添加了对允许隐式子类型(不需要继承)的结构子类型的支持。如果您有兴趣,我将其总结在一个单独的answer 中。

以上是关于如何以与静态类型检查兼容的方式实现接口?的主要内容,如果未能解决你的问题,请参考以下文章

默认方法

如何强制实现受保护的静态函数

从零开始的Java开发1-4-4 多态与内部类:接口:定义并测试抽象方法常量默认方法静态方法重名默认方法和重名静态方法的解决方案继承;成员静态方法匿名 内部类

Simple JavaJava中静态类型检查是如何进行的

Java静态方法获取所属类的信息

Java中静态类型检查是如何进行的