我可以使用 Mypy 存根作为接口吗?

Can I use Mypy stubs as interfaces?

Mypy允许我们编写class stubs,它可以和实际的class放在同一个目录下。这个存根与其他语言中已知的接口非常相似。是否可以让客户端使用存根并严格按照存根执行?

我想工作的例子:

class IDependency:
  def do_something(self) -> None: ...
  def do_something_else(self) -> None: ...

class Service:
  def __init__(self, dependency: IDependency):
    dependency.do_something()
    dependency.do_something_else() # this fails silently

class DependencyImplementation(IDependency):
  def do_something(self) -> None:
    print("doing something")

  # Note there is no `do_something_else` here.

这行得通。但是,如果 DependencyImplementation 没有实现 do_something 方法,Mypy 不会出错,Python 本身也不会出错。这个电话什么都不做。我是否必须编写 raise NotImplementedException() 或使用 @abc.abstractmethod 注释每个方法才能工作? Mypy 或 Python 解释器中是否有一些特殊标志?

这是 Mypy Protocols 的用例吗?它似乎很快就会到来(也许Python 4?)

这确实是您可以使用 @abc.abstractmethod 或协议来完成的事情。前者类似于使用Java的抽象classes;后者将类似于使用 Go 的接口或 Rust 特性。

这是一个使用 ABC 的例子:

from abc import abstractmethod

class Parent:
    @abstractmethod
    def foo(self) -> None: ...

# Missing an implementation for 'foo'!
class Child(Parent): pass

print(Child())  # Error: Cannot instantiate abstract class 'Child' with abstract attribute 'foo'

关于此示例的一些注意事项:

  1. 您在子 class 的 实例化 上遇到错误,而不是在声明上。这是为了支持您从不实例化 Child 而是再次 subclass 并在第二个 subclass.
  2. 中定义 foo 的用例
  3. 我们不需要将通常的 abc metaclass 添加到 Parent(例如 class Parent(metaclass=ABCMeta)):mypy 会理解 @abc.abstractmethod 有或没有它的含义.仅当您希望 Python 解释器也强制您已正确覆盖任何在运行时标记为抽象的内容时,才包含 metaclass。
  4. ABC 不完全是接口——您仍然可以定义字段和 non-abstract 方法。它们更类似于 Java-style 抽象 classes.

您也可以使用协议,但目前您需要先 pip install typing_extensions 才能使用它。这是一个例子:

from typing_extensions import Protocol

class CanFoo(Protocol):
    def foo(self) -> None: ...

class Child: pass

def expects_fooable(x: CanFoo) -> None: ...

x = Child()
expects_fooable(x)  # Error: Argument 1 to "expects_fooable" has incompatible type "Child"; expected "CanFoo"

一些注意事项:

  1. 这里,Child 故意不继承自 CanFoo:在 class 和它实现的协议之间没有明确的 link:协议是与 Go-style 接口非常相似,可以更 ad-hoc。将此与 Java 等语言进行对比,您确实需要在 class 定义中包含 "implements Blah"。
  2. 与之前的错误不同,我们在 Child 的实例化中没有遇到错误:它本身没有任何错误。相反,当我们尝试不正确地使用它时,我们会得到一个异常。

一些最后的说明:

  1. 存根文件可能表面上看起来像接口,但实际上并非如此:它们更像是一种将类型引入我们无法轻易修改和添加类型提示的代码的方式。您可以认为它们与 C-style 头文件有些相似:这是一种独立于源代码存储现有对象签名的方式。
  2. "Typeshed" 是 specific project 的名称,其中包括标准库的存根和一些流行的第 3 方模块。该词不是 "stub files" 的同义词。同样,术语 "class stub" 也有点用词不当:只有存根文件,其中可能包含也可能不包含 classes 的定义。 (如果您尝试键入的原始 Python 或 C 扩展库仅包含函数,则相应的存根文件也可能仅包含这些函数的签名。)