Correct way to hint that a class is implementing a Protocol?

jro*_*nds 10 python unit-testing typing

On a path of improvement for my Python dev work. I have interest in testing interfaces defined with Protocol at CI/deb building time, so that if a interface isn't actually implemented by a class we will know immediately after the unit tests run.

My approach was typing with Protocol and using implements runtime_checkable to build unit test. That works, but the team got into a little debate about how to indicate a concretion was implementing a Protocol without busting runtime_checkable. In C++/Java you need inheritance to indicate implementations of interfaces, but with Python you don't necessarily need inheritance. The conversation centered on whether we should be inheriting from a Protocol interface class.

考虑最后的这个代码示例,它提供了问题的大部分要点。我们正在考虑 Shape 并指示如何向未来的开发人员暗示 Shape 正在提供 IShape,但是通过继承这样做会使 isinstance 的 runtime_checkable 版本无法用于其在单元测试中的目的。

这里有几个改进的途径:

我们可以找到更好的方法来暗示 Shape 实现了 IShape,但不涉及直接继承。我们可以找到一种更好的方法来检查接口是否在测试 deb 包构建时实现。也许runtime_checkable是错误的想法。

有人获得有关如何更好地使用 Python 的指导吗?谢谢!


from typing import (
    Protocol,
    runtime_checkable
)
import dataclasses

@runtime_checkable
class IShape(Protocol):
    x: float


@dataclasses.dataclass
class Shape(IShape):
    foo:float = 0.

s  = Shape()
# evaluates as True but doesnt provide the interface. Undermines the point of the run-time checkable in unit testing
assert isinstance(s, IShape)
print(s.x)  # Error.  Interface wasnt implemented




#
# Contrast with this assert
#
@dataclasses.dataclass
class Goo():
    x:float = 1

@dataclasses.dataclass
class Hoo():
    foo: float = 1

g = Goo()
h = Hoo()
assert isinstance(g, IShape)  # asserts as true
# but because it has the interface and not because we inherited.
print(g.x)


assert isinstance(h, IShape)  # asserts as False which is what we want

Run Code Online (Sandbox Code Playgroud)

che*_*ner 9

当谈论静态类型检查时,它有助于理解子类型与子类不同的概念。(在 Python 中,类型和类是同义词;而在由诸如 之类的工具实现的类型系统中则不然mypy。)

如果我们明确地说,类型T类型的名义子类型。S子类化是名义子类型化的一种形式:T是 if 的子类型S(但不仅是ifT是 的子类S

如果类型本身与 兼容,则该类型T是类型的结构子类型。协议是 Python 对结构子类型的实现。不需要是(通过子类化)的名义子类型才能成为(通过具有属性)的结构子类型。STSShapeIShapeIShapex

因此,定义IShape为 的 而Protocol不仅仅是 的超类的目的Shape是支持结构子类型并避免对名义子类型的需要(以及继承可能引入的所有问题)。

class IShape(Protocol):
    x: float


# A structural subtype of IShape
# Not a nominal subtype of IShape
class Shape:
    def __init__(self):
        self.x = 3

# Not a structural subtype of IShape
class Unshapely:
    def __init__(self):
        pass


def foo(v: IShape):
    pass

foo(Shape())  # OK
foo(Unshapely())  # Not OK
Run Code Online (Sandbox Code Playgroud)

那么结构子类型可以替代名义子类型吗?一点也不。继承有其用途,但是当它是唯一的子类型方法时,它就会被不恰当地使用。一旦您区分了类型系统中的结构子类型和名义子类型,您就可以使用适合您实际需要的子类型。

  • 顺便说一句,[PEP544](https://peps.python.org/pep-0544/#explicitly-declaring-implementation)提到子类化是一种显式声明新类来实现协议的方法。这里另一件重要的事情是协议成员的实际继承,它可以类似于 ABC 来使用(协议可以提供默认属性值或方法实现)。如果您继承协议,则“mypy”可以在静态分析步骤中检测不兼容的属性,而无需使用“runtime_checkable”。 (3认同)