如何用Protocol进行鸭式输入?

1,112 阅读2分钟

鸭子类型说 "如果它像鸭子一样叫,就把它当成鸭子。"类型检查似乎与鸭子类型不一致:我们把变量限制在命名的类型(类)上,只允许该类型或子类型。 这似乎会阻止我们传递任意的对象,这些对象可以 "以正确的方式叫"。 但自从PEP 0544以来,我们可以用以下方式声明 "鸭子式 "类型 typing.Protocol.

以此为例。

class Duck:
    def quack(self) -> str:
        return "Quack."


def sonorize(duck: Duck) -> None:
    print(duck.quack())


sonorize(Duck())

这里我们有一个vanillaDuck 类和一个接受该类的函数。sonorize() 是相当限制性的,可以通过鸭子类型更好地服务于用户。它可以接受任何具有正确类型的quack() 方法,并且功能很好。

指定的方式是用一个 typing.Protocol一个协议类型包含一组类型化的方法和变量。 如果一个对象有这些方法和变量,它将与协议类型相匹配。

typing.Protocol 是在 Python 3.8 中添加的。 在之前的 Python 版本中,你可以使用 typing_extensions.Protocol,并在升级时进行迁移。Mypy从0.530(2017年10月)版本开始支持Protocol

在我们的例子中,我们可以为可以呱呱叫的对象定义一个协议。

from typing import Protocol


class Quacker(Protocol):
    def quack(self) -> str:
        ...

我们使用class 语法来定义一个协议,并在主体中添加带有类型提示的相关成员。这些方法将永远不会被执行,所以在主体中使用 Python 的省略号是标准的。

定义了协议后,我们可以在sonorize() 中使用它来匹配任何具有返回字符串的quack() 方法的对象。 要把它组合成一个完整的例子。

from typing import Protocol


class Quacker(Protocol):
    def quack(self) -> str:
        ...


class Duck:
    def quack(self) -> str:
        return "Quack."


class MegaDuck:
    def quack(self) -> str:
        return "QUACK!"


def sonorize(duck: Quacker) -> None:
    print(duck.quack())


sonorize(Duck())
sonorize(MegaDuck())

Python 文档称使用Protocol 结构子类型,意味着类型检查器根据它们的内部结构来比较类型。这与名义子类型相反,意味着类型检查器根据它们是否是命名的类型或子类来比较类型。

Python 的collections.abc 模块中的抽象基类对于常见的协议检查非常有用。例如,你可以使用Sized 来接受任何与len() 配合的类型,而Iterable[int] 则接受任何整数的可迭代类型。