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

2024-05-04 17:56:44 发布

您现在位置:Python中文网/ 问答频道 /正文

我有两个基类,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-的对象,即具有属性x和方法foo的对象。所以,如果objFoo一样走,如果它像Foo嘎嘎叫,那么{}就会很高兴。现在整个项目都使用类型提示,因此目前我指示obj: Foo。到现在为止,一直都还不错。在

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

^{pr2}$

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

^{3}$

使用抽象基类

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

import abc

class Fooish(abc.ABC):
    x : int

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

但是我不能让FooBarFooish继承,因为Bar有自己的元类,因此这会导致元类冲突。所以我想在Foo和{}上同时使用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”类的形式从abc.ABC继承,然后让Foo和{}从中继承:

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(abstract)基类现在是可实例化的,并且是Worker(...)的有效参数,尽管它没有提供属性x,所以没有意义。在

问题是

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


Tags: selfobj类型属性fooinitdefbar
3条回答

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

与所讨论的场景相关的是^{},正如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
  1. 要去掉error: Signature of "x" incompatible with supertype "Fooish",可以对x: typing.Any进行注释。在
  2. 要使Fooish真正抽象,需要一些技巧来解决元类冲突。我从this answer取了一份食谱:
class MyABCMeta(MyMeta, abc.ABCMeta):
    pass

之后,可以创建Fooish

^{pr2}$

在运行时成功执行且未显示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中定义{},它不调用MyMeta.__new__,而是调用abc.ABCMeta.__new__。但事情会变得复杂。。。所以,也许有非抽象的Fooish会更容易。在

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

包括以下内容:

^{pr2}$

参见:

相关问题 更多 >