如何以与静态类型检查兼容的方式实现接口?
How to implement an interface in a way that is compatible with static type checks?
我有两个基础 classes,Foo
和 Bar
,以及一个 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冲突。所以我考虑在 Foo
和 FooBar
上使用 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
继承的接口,然后同时拥有 Foo
和 FooBar
从中继承:
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
(抽象)基础 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
见:
- 要去掉
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
.
支持 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
我有两个基础 classes,Foo
和 Bar
,以及一个 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冲突。所以我考虑在 Foo
和 FooBar
上使用 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
继承的接口,然后同时拥有 Foo
和 FooBar
从中继承:
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
(抽象)基础 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
见:
- 要去掉
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
.
支持 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