Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 544 – 协议:结构子类型 (静态鸭子类型)

作者:
Ivan Levkivskyi <levkivskyi at gmail.com>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, Łukasz Langa <lukasz at python.org>
BDFL 代表:
Guido van Rossum <guido at python.org>
讨论组:
Python-Dev 邮件列表
状态:
最终
类型:
标准轨道
主题:
类型提示
创建时间:
2017 年 3 月 5 日
Python 版本:
3.8
决议:
Typing-SIG 消息

目录

注意

此 PEP 是一份历史文档:请参阅 协议typing.Protocol 以获取最新的规范和文档。规范化的类型规范维护在 类型规范网站 上;运行时类型行为在 CPython 文档中描述。

×

请参阅 类型规范更新流程,了解如何提出对类型规范的更改建议。

摘要

PEP 484 中引入的类型提示可用于指定静态类型检查器和其他第三方工具的类型元数据。但是,PEP 484 仅指定了名义子类型的语义。在本 PEP 中,我们指定了协议类的静态和运行时语义,这些协议类将提供对结构子类型 (静态鸭子类型) 的支持。

基本原理和目标

目前,PEP 484typing 模块 [typing] 为几个常见的 Python 协议(如 IterableSized)定义了抽象基类。它们的问题在于,类必须显式标记以支持它们,这很不pythonic,并且与人们在惯用的动态类型 Python 代码中通常所做的不同。例如,这符合 PEP 484

from typing import Sized, Iterable, Iterator

class Bucket(Sized, Iterable[int]):
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

用户定义的 ABC 也存在相同问题:它们必须显式子类化或注册。对于库类型来说,这样做尤其困难,因为类型对象可能隐藏在库实现的深处。此外,广泛使用 ABC 可能会带来额外的运行时成本。

本 PEP 的目的是通过允许用户在不显式使用基类的情况下编写上述代码来解决所有这些问题,从而允许静态类型检查器使用结构化 [wiki-structural] 子类型将 Bucket 隐式地视为 SizedIterable[int] 的子类型

from typing import Iterator, Iterable

class Bucket:
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

def collect(items: Iterable[int]) -> int: ...
result: int = collect(Bucket())  # Passes type check

请注意,typing 模块中的 ABC 在运行时已经提供了结构化行为,isinstance(Bucket(), Iterable) 返回 True。本提案的主要目标是在静态环境中支持这种行为。对于用户定义的协议,将提供相同的函数,如下所述。使用协议类的上述代码更符合常见的 Python 约定。它也是自动可扩展的,并且适用于恰好实现了所需协议的其他无关类。

名义子类型与结构子类型

结构子类型对于 Python 程序员来说是自然的,因为它与鸭子类型的运行时语义相匹配:具有某些属性的对象将独立于其实际运行时类进行处理。但是,正如在 PEP 483 中所讨论的,名义子类型和结构子类型都有其优点和缺点。因此,在本 PEP 中,我们不建议用结构子类型完全替换 PEP 484 中描述的名义子类型。相反,在本 PEP 中指定的协议类是对普通类的补充,用户可以自由选择应用特定解决方案的位置。有关详细信息,请参阅本 PEP 末尾关于 被拒绝 的想法部分。

非目标

在运行时,协议类将是简单的 ABC。没有打算为协议类提供复杂的运行时实例和类检查。这样做既困难又容易出错,并且会与 PEP 484 的逻辑相矛盾。此外,遵循 PEP 484PEP 526,我们声明协议是完全可选的

  • 对于使用协议类注释的变量或参数,不会强制执行任何运行时语义。
  • 任何检查都将仅由第三方类型检查器和其他工具执行。
  • 即使使用类型注释,程序员也可以自由选择不使用它们。
  • 将来没有打算使协议成为必需。

重申一下,为协议类提供复杂的运行时语义不是本 PEP 的目标,主要目标是为静态结构子类型提供支持和标准。在运行时环境中将协议用作 ABC 的可能性只是一个次要好处,其主要目的是为已经使用 ABC 的项目提供无缝过渡。

现有的结构子类型方法

在描述实际规范之前,我们回顾并评论与 Python 和其他语言中的结构子类型相关的现有方法

  • zope.interface [zope-interfaces] 是 Python 中第一个广泛使用的结构子类型方法之一。它是通过提供特殊类来实现的,这些类用于区分接口类和普通类,用于标记接口属性,以及显式声明实现。例如
    from zope.interface import Interface, Attribute, implementer
    
    class IEmployee(Interface):
    
        name = Attribute("Name of employee")
    
        def do(work):
            """Do some work"""
    
    @implementer(IEmployee)
    class Employee:
    
        name = 'Anonymous'
    
        def do(self, work):
            return work.start()
    

    Zope 接口支持接口类的各种契约和约束。例如

    from zope.interface import invariant
    
    def required_contact(obj):
        if not (obj.email or obj.phone):
            raise Exception("At least one contact info is required")
    
    class IPerson(Interface):
    
        name = Attribute("Name")
        email = Attribute("Email Address")
        phone = Attribute("Phone Number")
    
        invariant(required_contact)
    

    甚至支持更详细的不变式。但是,Zope 接口完全依赖于运行时验证。这种对运行时属性的关注超出了当前提案的范围,并且静态支持不变式可能难以实现。但是,使用特殊基类标记接口类的想法是合理的,并且在静态和运行时都很容易实现。

  • Python 抽象基类 [abstract-classes] 是标准库工具,用于提供与结构子类型类似的功能。这种方法的缺点是需要显式子类化抽象类或注册实现
    from abc import ABC
    
    class MyTuple(ABC):
        pass
    
    MyTuple.register(tuple)
    
    assert issubclass(tuple, MyTuple)
    assert isinstance((), MyTuple)
    

    正如在 基本原理 中提到的,我们希望避免这种必要性,尤其是在静态环境中。但是,在运行时环境中,ABC 是协议类的良好候选者,并且它们已经在 typing 模块中被广泛使用。

  • collections.abc 模块 [collections-abc] 中定义的抽象类稍微高级一些,因为它们实现了一个自定义的 __subclasshook__() 方法,该方法允许在没有显式注册的情况下进行运行时结构化检查
    from collections.abc import Iterable
    
    class MyIterable:
        def __iter__(self):
            return []
    
    assert isinstance(MyIterable(), Iterable)
    

    这种行为似乎非常适合协议的运行时和静态行为。正如在 基本原理 中所讨论的,我们建议为这种行为添加静态支持。此外,为了允许用户为用户定义的协议实现这种运行时行为,将提供一个特殊的 @runtime_checkable 装饰器,请参阅下面的详细 讨论

  • TypeScript [typescript] 支持用户定义的类和接口。不需要显式实现声明,并且结构子类型在静态地进行验证。例如
    interface LabeledItem {
        label: string;
        size?: int;
    }
    
    function printLabel(obj: LabeledItem) {
        console.log(obj.label);
    }
    
    let myObj = {size: 10, label: "Size 10 Object"};
    printLabel(myObj);
    

    请注意,支持可选的接口成员。此外,TypeScript 禁止实现中出现冗余成员。虽然可选成员的想法很有趣,但它会使本提案变得复杂,并且不清楚它有多有用。因此,建议推迟此事;请参阅 被拒绝 的想法。总的来说,在没有运行时影响的情况下进行静态协议检查的想法看起来很合理,并且本提案基本上遵循相同的思路。

  • Go [golang] 采用了一种更激进的方法,使接口成为提供类型信息的首要方式。此外,赋值用于明确确保实现
    type SomeInterface interface {
        SomeMethod() ([]byte, error)
    }
    
    if _, ok := someval.(SomeInterface); ok {
        fmt.Printf("value implements some interface")
    }
    

    这两种想法在该提案的背景下都值得商榷。请参阅关于 被拒绝 的想法部分。

规范

术语

我们建议使用术语协议来表示支持结构性子类型的类型。原因是,例如,迭代器协议一词在社区中被广泛理解,在静态类型化上下文中为这个概念想出一个新词只会造成混淆。

这样做有一个缺点,即协议一词将被赋予两种略微不同的含义:第一个是传统、众所周知但略显模糊的协议概念,例如迭代器;第二个是静态类型化代码中定义更明确的协议概念。在大多数情况下,这种区别并不重要,而在其他情况下,我们建议在引用静态类型概念时添加限定词,例如协议类

如果一个类在其 MRO 中包含一个协议,那么该类被称为该协议的显式子类。如果一个类是某个协议的结构性子类型,则说它实现了该协议并且与该协议兼容。如果一个类与一个协议兼容,但该协议未包含在 MRO 中,则该类是该协议的隐式子类型。(注意,如果一个类在子类中将协议属性设置为 None,则可以显式地子类化协议但仍不实现它,请参阅 Python [data-model] 获取详细信息。)

协议的属性(变量和方法)对于其他类来说是强制性的,以便被视为结构性子类型,这些属性被称为协议成员。

定义协议

协议通过在基类列表中包含一个新的特殊类 typing.Protocolabc.ABCMeta 的实例)来定义,通常位于列表的末尾。以下是一个简单的示例

from typing import Protocol

class SupportsClose(Protocol):
    def close(self) -> None:
        ...

现在,如果定义一个类 Resource,它具有一个具有兼容签名的 close() 方法,它将隐式地成为 SupportsClose 的子类型,因为结构性子类型用于协议类型

class Resource:
    ...
    def close(self) -> None:
        self.file.close()
        self.lock.release()

除了下面明确提到的少数限制之外,协议类型可以在任何可以使用普通类型的上下文中使用

def close_all(things: Iterable[SupportsClose]) -> None:
    for t in things:
        t.close()

f = open('foo.txt')
r = Resource()
close_all([f, r])  # OK!
close_all([1])     # Error: 'int' has no 'close' method

请注意,用户定义的类 Resource 和内置 IO 类型(open() 的返回值类型)都被认为是 SupportsClose 的子类型,因为它们提供了具有兼容类型签名的 close() 方法。

协议成员

在协议类主体中定义的所有方法都是协议成员,包括普通方法和用 @abstractmethod 装饰的方法。如果协议方法的任何参数没有标注,则假定它们的类型为 Any(请参阅 PEP 484)。协议方法的主体将进行类型检查。不应该通过 super() 调用的抽象方法应该抛出 NotImplementedError。示例

from typing import Protocol
from abc import abstractmethod

class Example(Protocol):
    def first(self) -> int:     # This is a protocol member
        return 42

    @abstractmethod
    def second(self) -> int:    # Method without a default implementation
        raise NotImplementedError

静态方法、类方法和属性在协议中同样允许。

要定义一个协议变量,可以使用 PEP 526 在类主体中进行变量标注。仅在方法主体中通过 self 赋值定义的附加属性是不允许的。这样做的理由是,协议类实现通常不被子类型共享,因此接口不应依赖于默认实现。示例

from typing import Protocol, List

class Template(Protocol):
    name: str        # This is a protocol member
    value: int = 0   # This one too (with default)

    def method(self) -> None:
        self.temp: List[int] = [] # Error in type checker

class Concrete:
    def __init__(self, name: str, value: int) -> None:
        self.name = name
        self.value = value

    def method(self) -> None:
        return

var: Template = Concrete('value', 42)  # OK

要区分协议类变量和协议实例变量,应该使用特殊的 ClassVar 标注,如 PEP 526 所指定。默认情况下,如上定义的协议变量被认为是可读写的。要定义一个只读协议变量,可以使用(抽象)属性。

显式声明实现

要显式声明某个类实现了给定协议,可以使用它作为常规基类。在这种情况下,一个类可以使用协议成员的默认实现。静态分析工具预计会自动检测到一个类是否实现了给定协议。因此,虽然可以显式地子类化协议,但为了类型检查,没有必要这样做。

如果子类型关系是隐式的并且仅通过结构性子类型,则不能使用默认实现 - 继承的语义没有改变。示例

class PColor(Protocol):
    @abstractmethod
    def draw(self) -> str:
        ...
    def complex_method(self) -> int:
        # some complex code here
        ...

class NiceColor(PColor):
    def draw(self) -> str:
        return "deep blue"

class BadColor(PColor):
    def draw(self) -> str:
        return super().draw()  # Error, no default implementation

class ImplicitColor:   # Note no 'PColor' base here
    def draw(self) -> str:
        return "probably gray"
    def complex_method(self) -> int:
        # class needs to implement this
        ...

nice: NiceColor
another: ImplicitColor

def represent(c: PColor) -> None:
    print(c.draw(), c.complex_method())

represent(nice) # OK
represent(another) # Also OK

请注意,显式子类型和隐式子类型之间几乎没有区别,显式子类化的主要好处是可以“免费”获得一些协议方法。此外,类型检查器可以静态地验证该类是否确实正确地实现了该协议

class RGB(Protocol):
    rgb: Tuple[int, int, int]

    @abstractmethod
    def intensity(self) -> int:
        return 0

class Point(RGB):
    def __init__(self, red: int, green: int, blue: str) -> None:
        self.rgb = red, green, blue  # Error, 'blue' must be 'int'

    # Type checker might warn that 'intensity' is not defined

一个类可以显式地从多个协议继承,也可以从普通类继承。在这种情况下,方法使用普通的 MRO 解析,类型检查器会验证所有子类型是否正确。 @abstractmethod 的语义没有改变,它们都必须在显式子类可以实例化之前被实现。

合并和扩展协议

总的哲学思想是,协议大多类似于普通的 ABC,但静态类型检查器会对它们进行特殊处理。子类化协议类不会将子类变成协议,除非它也具有 typing.Protocol 作为显式基类。没有这个基类,该类就会被“降级”为一个普通的 ABC,不能与结构性子类型一起使用。这样做的理由是我们不想让某个类仅仅因为它的一个基类碰巧是一个协议而意外地充当协议。在静态类型化世界中,我们仍然稍微更喜欢名义子类型而不是结构性子类型。

子协议可以通过同时具有一个或多个协议作为直接基类并且具有 typing.Protocol 作为直接基类来定义

from typing import Sized, Protocol

class SizedAndClosable(Sized, Protocol):
    def close(self) -> None:
        ...

现在,协议 SizedAndClosable 是一个具有两种方法的协议,__len__close。如果在基类列表中省略了 Protocol,则这将是一个必须实现 Sized 的普通(非协议)类。或者,可以通过将 定义 部分示例中的 SupportsClose 协议与 typing.Sized 合并来实现 SizedAndClosable 协议

from typing import Sized

class SupportsClose(Protocol):
    def close(self) -> None:
        ...

class SizedAndClosable(Sized, SupportsClose, Protocol):
    pass

SizedAndClosable 的这两个定义是等价的。在考虑子类型时,协议之间的子类关系没有意义,因为结构兼容性是标准,而不是 MRO。

如果在基类列表中包含了 Protocol,则所有其他基类必须是协议。协议不能扩展普通类,请参阅 被拒绝 的想法了解其原因。请注意,围绕显式子类化的规则与普通的 ABC 不同,在普通的 ABC 中,抽象性仅仅是通过至少有一个抽象方法没有被实现来定义的。协议类必须被显式地标记。

泛型协议

泛型协议很重要。例如,SupportsAbsIterableIterator 是泛型协议。它们的定义方式类似于普通的非协议泛型类型

class Iterable(Protocol[T]):
    @abstractmethod
    def __iter__(self) -> Iterator[T]:
        ...

Protocol[T, S, ...] 被允许作为 Protocol, Generic[T, S, ...] 的简写形式。

用户定义的泛型协议支持显式声明的方差。如果推断的方差与声明的方差不同,类型检查器将发出警告。示例

T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)

class Box(Protocol[T_co]):
    def content(self) -> T_co:
        ...

box: Box[float]
second_box: Box[int]
box = second_box  # This is OK due to the covariance of 'Box'.

class Sender(Protocol[T_contra]):
    def send(self, data: T_contra) -> int:
        ...

sender: Sender[float]
new_sender: Sender[int]
new_sender = sender  # OK, 'Sender' is contravariant.

class Proto(Protocol[T]):
    attr: T  # this class is invariant, since it has a mutable attribute

var: Proto[float]
another_var: Proto[int]
var = another_var  # Error! 'Proto[float]' is incompatible with 'Proto[int]'.

请注意,与名义类不同,事实上协变的协议不能声明为不变的,因为这可能会破坏子类型的传递性(请参阅 被拒绝 的想法以了解详细信息)。例如

T = TypeVar('T')

class AnotherBox(Protocol[T]):  # Error, this protocol is covariant in T,
    def content(self) -> T:     # not invariant.
        ...

递归协议

递归协议也受支持。对协议类名的前向引用可以作为字符串给出,如 PEP 484 所指定。递归协议对于以抽象方式表示自引用数据结构(如树)很有用

class Traversable(Protocol):
    def leaves(self) -> Iterable['Traversable']:
        ...

请注意,对于递归协议,在一个类是该协议的子类型的判断依赖于自身的场景中,该类被认为是该协议的子类型。继续前面的示例

class SimpleTree:
    def leaves(self) -> List['SimpleTree']:
        ...

root: Traversable = SimpleTree()  # OK

class Tree(Generic[T]):
    def leaves(self) -> List['Tree[T]']:
        ...

def walk(graph: Traversable) -> None:
    ...
tree: Tree[float] = Tree()
walk(tree)  # OK, 'Tree[float]' is a subtype of 'Traversable'

协议中的自类型

协议中的自类型遵循 相应的规范 PEP 484。例如

C = TypeVar('C', bound='Copyable')
class Copyable(Protocol):
    def copy(self: C) -> C:

class One:
    def copy(self) -> 'One':
        ...

T = TypeVar('T', bound='Other')
class Other:
    def copy(self: T) -> T:
        ...

c: Copyable
c = One()  # OK
c = Other()  # Also OK

回调协议

协议可以用来定义灵活的回调类型,这些类型很难(甚至不可能)使用 PEP 484 中指定的 Callable[...] 语法来表达,例如可变参数、重载和复杂的泛型回调。它们可以被定义为具有 __call__ 成员的协议

from typing import Optional, List, Protocol

class Combiner(Protocol):
    def __call__(self, *vals: bytes,
                 maxlen: Optional[int] = None) -> List[bytes]: ...

def good_cb(*vals: bytes, maxlen: Optional[int] = None) -> List[bytes]:
    ...
def bad_cb(*vals: bytes, maxitems: Optional[int]) -> List[bytes]:
    ...

comb: Combiner = good_cb  # OK
comb = bad_cb  # Error! Argument 2 has incompatible type because of
               # different name and kind in the callback

回调协议和 Callable[...] 类型可以互换使用。

使用协议

与其他类型的子类型关系

协议不能被实例化,因此不存在运行时类型为协议的值。对于具有协议类型的变量和参数,子类型关系受以下规则约束

  • 协议永远不是具体类型的子类型。
  • 具体类型 X 是协议 P 的子类型,当且仅当 X 使用兼容类型实现了 P 的所有协议成员。换句话说,关于协议的子类型始终是结构性的。
  • 如果协议 P1 定义了所有协议成员 P2 与兼容类型,则协议 P1 是另一个协议 P2 的子类型。

泛型协议类型遵循与非协议类型相同的变异规则。协议类型可以在任何其他类型可以使用的所有上下文中使用,例如在 UnionClassVar、类型变量边界等中。泛型协议遵循泛型抽象类的规则,只是使用结构兼容性代替继承关系定义的兼容性。

静态类型检查器将识别协议实现,即使相应的协议未导入

# file lib.py
from typing import Sized

T = TypeVar('T', contravariant=True)
class ListLike(Sized, Protocol[T]):
    def append(self, x: T) -> None:
        pass

def populate(lst: ListLike[int]) -> None:
    ...

# file main.py
from lib import populate  # Note that ListLike is NOT imported

class MockStack:
    def __len__(self) -> int:
        return 42
    def append(self, x: int) -> None:
        print(x)

populate([1, 2, 3])    # Passes type check
populate(MockStack())  # Also OK

协议的联合和交集

协议类 Union 的行为与非协议类相同。例如

from typing import Union, Optional, Protocol

class Exitable(Protocol):
    def exit(self) -> int:
        ...
class Quittable(Protocol):
    def quit(self) -> Optional[int]:
        ...

def finish(task: Union[Exitable, Quittable]) -> int:
    ...
class DefaultJob:
    ...
    def quit(self) -> int:
        return 0
finish(DefaultJob()) # OK

可以使用多重继承来定义协议的交集。示例

from typing import Iterable, Hashable

class HashableFloats(Iterable[float], Hashable, Protocol):
    pass

def cached_func(args: HashableFloats) -> float:
    ...
cached_func((1, 2, 3)) # OK, tuple is both hashable and iterable

如果这被证明是一个广泛使用的场景,那么将来可以根据 PEP 483 添加一个特殊的交集类型构造,有关更多详细信息,请参阅 拒绝 的想法。

Type[] 和类对象与协议

Type[Proto] 注释的变量和参数只接受 Proto 的具体(非协议)子类型。这样做的主要原因是允许使用此类类型实例化参数。例如

class Proto(Protocol):
    @abstractmethod
    def meth(self) -> int:
        ...
class Concrete:
    def meth(self) -> int:
        return 42

def fun(cls: Type[Proto]) -> int:
    return cls().meth() # OK
fun(Proto)              # Error
fun(Concrete)           # OK

相同的规则适用于变量

var: Type[Proto]
var = Proto    # Error
var = Concrete # OK
var().meth()   # OK

如果未显式类型化,则允许将 ABC 或协议类分配给变量,并且这种分配会创建一个类型别名。对于普通(非抽象)类,Type[] 的行为不会改变。

如果访问它上的所有成员导致与协议成员兼容的类型,则类对象被视为协议的实现。例如

from typing import Any, Protocol

class ProtoA(Protocol):
    def meth(self, x: int) -> int: ...
class ProtoB(Protocol):
    def meth(self, obj: Any, x: int) -> int: ...

class C:
    def meth(self, x: int) -> int: ...

a: ProtoA = C  # Type check error, signatures don't match!
b: ProtoB = C  # OK

NewType() 和类型别名

协议本质上是匿名的。为了强调这一点,静态类型检查器可能会拒绝 NewType() 内的协议类,以避免出现提供不同类型的错觉

from typing import NewType, Protocol, Iterator

class Id(Protocol):
    code: int
    secrets: Iterator[bytes]

UserId = NewType('UserId', Id)  # Error, can't provide distinct type

相反,类型别名得到完全支持,包括泛型类型别名

from typing import TypeVar, Reversible, Iterable, Sized

T = TypeVar('T')
class SizedIterable(Iterable[T], Sized, Protocol):
    pass
CompatReversible = Union[Reversible[T], SizedIterable[T]]

模块作为协议的实现

如果给定模块的公共接口与预期协议兼容,则接受模块对象,前提是预期协议。例如

# file default_config.py
timeout = 100
one_flag = True
other_flag = False

# file main.py
import default_config
from typing import Protocol

class Options(Protocol):
    timeout: int
    one_flag: bool
    other_flag: bool

def setup(options: Options) -> None:
    ...

setup(default_config)  # OK

要确定模块级函数的兼容性,将删除相应协议方法的 self 参数。例如

# callbacks.py
def on_error(x: int) -> None:
    ...
def on_success() -> None:
    ...

# main.py
import callbacks
from typing import Protocol

class Reporter(Protocol):
    def on_error(self, x: int) -> None:
        ...
    def on_success(self) -> None:
        ...

rp: Reporter = callbacks  # Passes type check

@runtime_checkable 装饰器和通过 isinstance() 缩小类型

默认语义是 isinstance()issubclass() 对于协议类型失败。这是鸭子类型的精神——协议基本上用于静态地建模鸭子类型,而不是在运行时显式地建模。

但是,协议类型应该能够实现自定义实例和类检查,当有意义时,类似于 Iterablecollections.abctyping 中的其他 ABC 的工作方式,但这仅限于非泛型和未下标的泛型协议 (Iterable 在静态上等效于 Iterable[Any])。typing 模块将定义一个特殊的 @runtime_checkable 类装饰器,它为类和实例检查提供与 collections.abc 类相同的语义,实质上使它们成为“运行时协议”

from typing import runtime_checkable, Protocol

@runtime_checkable
class SupportsClose(Protocol):
    def close(self):
        ...

assert isinstance(open('some/file'), SupportsClose)

请注意,实例检查在静态上并不完全可靠,这就是这种行为是选择加入的原因,有关示例,请参阅有关 拒绝 想法的部分。大多数类型检查器所能做的就是将 isinstance(obj, Iterator) 粗略地视为编写 hasattr(x, '__iter__') and hasattr(x, '__next__') 的更简单方法。为了最大程度地减少此功能的风险,将应用以下规则。

定义:

  • 数据和非数据协议:如果协议仅包含方法作为成员(例如 SizedIterator 等),则称为非数据协议。包含至少一个非方法成员(如 x: int)的协议称为数据协议。
  • 不安全的重叠:如果类型 X 不是 P 的子类型,但它是 P 的类型擦除版本的子类型,其中所有成员的类型为 Any,则类型 X 被称为与协议 P 不安全地重叠。此外,如果并集的至少一个元素与协议 P 不安全地重叠,则整个并集与 P 不安全地重叠。

规范:

  • 只有在协议被 @runtime_checkable 装饰器显式选择加入的情况下,协议才能用作 isinstance()issubclass() 中的第二个参数。存在此要求是因为协议检查在动态设置属性的情况下不安全,并且因为类型检查器只能证明 isinstance() 检查仅对给定类是安全的,而不是对所有子类都是安全的。
  • isinstance() 可以与数据协议和非数据协议一起使用,而 issubclass() 只能与非数据协议一起使用。这种限制存在,因为某些数据属性可以在构造函数中为实例设置,并且此信息并不总是可以在类对象上使用。
  • 如果第一个参数的类型与协议之间存在不安全的重叠,则类型检查器应拒绝 isinstance()issubclass() 调用。
  • 类型检查器应该能够在安全 isinstance()issubclass() 调用后从并集中选择正确的元素。对于从非并集类型进行缩小,类型检查器可以使用它们最好的判断(这是有意未指定的,因为精确的规范将需要交集类型)。

在 Python 2.7 - 3.5 中使用协议

变量注释语法是在 Python 3.6 中添加的,因此,如果需要支持早期版本,则无法使用 规范 部分中提出的定义协议变量的语法。为了以与 Python 的旧版本兼容的方式定义这些,可以使用属性。如果需要,属性可以是可设置的和/或抽象的

class Foo(Protocol):
    @property
    def c(self) -> int:
        return 42         # Default value can be provided for property...

    @abstractproperty
    def d(self) -> int:   # ... or it can be abstract
        return 0

此外,函数类型注释可以根据 PEP 484 使用(例如,为了提供与 Python 2 的兼容性)。本 PEP 中提出的 typing 模块更改也将通过目前在 PyPI 上提供的移植版本移植到早期版本。

协议类的运行时实现

实现细节

运行时实现可以在纯 Python 中完成,不会对核心解释器和标准库产生任何影响,除了 typing 模块和对 collections.abc 的少量更新。

  • 定义类 typing.Protocol,类似于 typing.Generic
  • 实现功能以检测类是否为协议。如果满足此条件,则添加一个类属性 _is_protocol = True。验证协议类在 MRO 中只有协议基类(除了对象)。
  • 实现 @runtime_checkable,它允许 __subclasshook__() 执行结构化实例和子类检查,如 collections.abc 类中一样。
  • 所有结构化子类型检查将由静态类型检查器执行,例如 mypy [mypy]。运行时不会提供对协议验证的额外支持。

typing 模块中的变更

typing 模块中的以下类将是协议

  • 可调用
  • 可等待
  • IterableIterator
  • AsyncIterableAsyncIterator
  • 可哈希
  • 尺寸
  • 容器
  • 集合
  • 可逆
  • ContextManagerAsyncContextManager
  • SupportsAbs(和其他 Supports* 类)

大多数这些类很小且概念上很简单。很容易看出这些协议实现了哪些方法,并立即识别出相应的运行时协议对应方。实际上,typing 中不需要进行太多更改,因为其中一些类在运行时已经表现出必要的方式。大多数这些将只需要在相应的 typeshed 存根 [typeshed] 中更新。

所有其他具体泛型类,例如 ListSetIODeque 等,足够复杂,因此有必要将它们保持为非协议(即,要求代码明确说明它们)。此外,很容易意外地遗漏一些方法未实现,并且显式标记子类关系允许类型检查器查明缺少的实现。

内省

现有的类内省机制(例如 dir__annotations__ 等)可以与协议一起使用。此外,在 typing 模块中实现的所有内省工具都将支持协议。由于根据此提案,所有属性都需要在类体中定义,因此协议类将比常规类具有更好的内省视角,在常规类中,属性可以隐式定义——协议属性不能以内省不可见的方式初始化(使用 setattr()、通过 self 赋值等)。不过,在 Python 3.5 及更早版本中,某些内容(如属性的类型)在运行时将不可见,但这似乎是一个合理的限制。

如上所述,对 isinstance()issubclass() 的支持将非常有限(对于带下标的泛型协议,这些将始终TypeError 失败,因为在这种情况下,无法在运行时给出可靠的答案)。但与其他内省工具一起,这为运行时类型检查工具提供了合理的视角。

被拒绝/推迟的想法

本节中的想法之前曾在 [several] [discussions] [elsewhere] 中讨论过。

默认情况下使每个类都成为协议

某些语言(如 Go)将结构子类型作为唯一或主要形式的子类型。我们可以通过默认将所有类都设为协议(甚至始终如此)来实现类似的结果。但是,我们认为最好要求显式标记类为协议,原因如下

  • 协议不具备常规类的一些属性。特别是,isinstance()(如针对普通类定义的那样)基于名义层次结构。为了将所有内容默认设为协议,并使 isinstance() 能够工作,将需要更改其语义,而这不会发生。
  • 协议类通常不应该有太多方法实现,因为它们描述的是接口,而不是实现。大多数类都有很多方法实现,这使得它们不适合作为协议类。
  • 经验表明,许多类实际上并不适合作为协议,主要是因为它们的接口太大、太复杂或太面向实现(例如,它们可能包括事实上的私有属性和方法,没有 __ 前缀)。
  • 现有 Python 代码中最有用的协议大多是隐式的。typingcollections.abc 中的 ABC 比较特殊,但它们也是 Python 最近的添加,大多数程序员还没有使用它们。
  • 许多内置函数只接受 int 的具体实例(及其子类实例),其他内置类也类似。在没有对 Python 运行时进行重大更改的情况下,将 int 设为结构类型是不安全的,而这不会发生。

协议子类化普通类

禁止这种情况的主要理由是为了保留子类型的传递性,请考虑以下示例

from typing import Protocol

class Base:
    attr: str

class Proto(Base, Protocol):
    def meth(self) -> int:
        ...

class C:
    attr: str
    def meth(self) -> int:
        return 0

现在,CProto 的子类型,而 ProtoBase 的子类型。但 C 不能是 Base 的子类型(因为后者不是协议)。这种情况非常奇怪。此外,关于 Base 的属性是否应该成为 Proto 的协议成员,存在歧义。

支持可选的协议成员

我们可以提出一些例子,在这些例子中,能够说一个方法或数据属性不必出现在实现协议的类中,但如果存在,则必须符合特定的签名或类型,会很有用。可以使用 hasattr() 检查来确定是否可以在特定实例上使用该属性。

TypeScript 等语言具有类似的功能,而且它们似乎非常常用。Python 中协议的当前现实潜在用例不需要这些功能。为了简单起见,我们建议不支持可选方法或属性。如果确实有需要,我们以后可以随时重新考虑这一点。

仅允许协议方法并强制使用 getter 和 setter

有人可能会争辩说协议通常只定义方法,而不是变量。但是,在只需要一个简单变量的情况下使用 getter 和 setter 会非常不符合 Python 风格。此外,在大型代码库中广泛使用属性(通常充当类型验证器)部分归因于以前缺乏针对 Python 的静态类型检查器,而这正是 PEP 484 和此 PEP 要解决的问题。例如

# without static types

class MyClass:
    @property
    def my_attr(self):
        return self._my_attr
    @my_attr.setter
    def my_attr(self, value):
        if not isinstance(value, int):
            raise ValidationError("An integer expected for my_attr")
        self._my_attr = value

# with static types

class MyClass:
    my_attr: int

支持非协议成员

曾经有一个想法是让一些方法成为“非协议”(即不需要实现,并且在显式子类化中继承),但这个想法被拒绝了,因为它会使事情变得复杂。例如,考虑以下情况

class Proto(Protocol):
    @abstractmethod
    def first(self) -> int:
        raise NotImplementedError
    def second(self) -> int:
        return self.first() + 1

def fun(arg: Proto) -> None:
    arg.second()

问题是这是否应该是一个错误?我们认为大多数人都会期望这种情况是有效的。因此,为了安全起见,我们需要要求在隐式子类中实现这两个方法。此外,如果查看 collections.abc 中的定义,很少有方法可以被认为是“非协议”。因此,决定不引入“非协议”方法。

这样做只有一个缺点:对于“大型”协议的隐式子类型,将需要一些样板代码。但是,这并不适用于所有都是“小型”(即只有少量抽象方法)的“内置”协议。此外,这种风格不建议用于用户定义的协议。建议创建紧凑的协议并组合它们。

使协议与其他方法互操作

这里描述的协议基本上是对现有 ABC 概念的最小扩展。我们认为,这应该是理解它们的方式,而不是理解它们是取代 Zope 接口的东西,例如。尝试这种互操作性将极大地使概念和实现复杂化。

另一方面,Zope 接口在概念上是这里定义的协议的超集,但使用不兼容的语法来定义它们,因为在 PEP 526 之前,没有直接的方法来注释属性。在 3.6+ 世界中,zope.interface 可能会采用 Protocol 语法。在这种情况下,可以教会类型检查器将接口识别为协议,并对它们进行简单的结构检查。

使用赋值来显式检查类是否实现了协议

在 Go 语言中,显式实现检查是通过虚拟赋值执行的 [golang]。这种方法在当前提案中也是可能的。示例

class A:
    def __len__(self) -> float:
        return ...

_: Sized = A()  # Error: A.__len__ doesn't conform to 'Sized'
                # (Incompatible return type 'float')

这种方法将检查从类定义转移出去,它几乎需要一个注释,否则代码可能对普通读者毫无意义——它看起来像死代码。此外,在最简单的情况下,它需要构造 A 的实例,如果这需要访问或分配某些资源(如文件或套接字),则可能会有问题。我们可以通过使用强制转换来解决后者,例如,但这会使代码变得难看。因此,我们不鼓励使用这种模式。

默认情况下支持 isinstance() 检查

这样做的问题是实例检查可能不可靠,除非存在像 Iterable 这样的通用签名约定。例如

class P(Protocol):
    def common_method_name(self, x: int) -> int: ...

class X:
    <a bunch of methods>
    def common_method_name(self) -> None: ... # Note different signature

def do_stuff(o: Union[P, X]) -> int:
    if isinstance(o, P):
        return o.common_method_name(1)  # Results in TypeError not caught
                                        # statically if o is an X instance.

另一个潜在的问题是实例化之后分配属性

class P(Protocol):
    x: int

class C:
    def initialize(self) -> None:
        self.x = 0

c = C()
isinstance(c, P)  # False
c.initialize()
isinstance(c, P)  # True

def f(x: Union[P, int]) -> None:
    if isinstance(x, P):
        # Static type of x is P here.
        ...
    else:
        # Static type of x is int, but can be other type at runtime...
        print(x + 1)

f(C())  # ...causing a TypeError.

我们认为要求显式类装饰器会更好,因为这样就可以在文档中添加有关此类问题的警告。用户将能够评估每个协议的利弊是否大于潜在的混淆,并显式选择加入——但默认行为会更安全。最后,如果需要,将默认行为设为默认行为将很容易,而将其设为选择加入则可能有问题。

提供一个特殊的交集类型构造

曾经有一个想法是允许 Proto = All[Proto1, Proto2, ...] 作为以下内容的简写

class Proto(Proto1, Proto2, ..., Protocol):
    pass

但是,目前还不清楚它会流行到什么程度/有用到什么程度,并且在非协议类中为类型检查器实现此功能可能很困难。最后,如果需要,稍后添加此功能将非常容易。

禁止非协议类显式子类化协议

此建议被拒绝的原因如下

  • 向后兼容性:人们已经在使用 ABC,包括来自 typing 模块的泛型 ABC。如果禁止显式子类化这些 ABC,那么相当多的代码将无法运行。
  • 便利性:存在类似协议的 ABC(它们可能被转换为协议),这些 ABC 具有许多有用的“mixin”(非抽象)方法。例如,在 Sequence 的情况下,只需要在显式子类中实现 __getitem____len__,就可以免费获得 __iter____contains____reversed__indexcount
  • 显式子类化明确表明一个类实现了特定协议,这使得子类型关系更容易看清楚。
  • 类型检查器可以更容易地警告缺少的协议成员或类型不兼容的成员,而无需使用上面本节中讨论的虚拟赋值之类的技巧。
  • 显式子类化使得能够强制将一个类视为协议的子类型(通过使用 # type: ignore 以及显式基类),即使它并不严格兼容,例如,当它具有不安全的覆盖时。

可变属性的协变子类型

被拒绝,因为可变属性的协变子类型是不安全的。考虑以下示例

class P(Protocol):
    x: float

def f(arg: P) -> None:
    arg.x = 0.42

class C:
    x: int

c = C()
f(c)  # Would typecheck if covariant subtyping
      # of mutable attributes were allowed.
c.x >> 1  # But this fails at runtime

最初建议出于实际原因允许这样做,但随后被拒绝,因为这可能会掩盖一些难以发现的错误。

覆盖协议类的推断方差

有人建议,如果协议实际上是协变或逆变,则允许将其声明为不变(因为这对于名义类是可能的,请参阅 PEP 484)。但是,决定不这样做是因为它有以下几个缺点

  • 声明的协议不变性破坏了子类型的传递性。考虑以下情况
    T = TypeVar('T')
    
    class P(Protocol[T]):  # Protocol is declared as invariant.
        def meth(self) -> T:
            ...
    class C:
        def meth(self) -> float:
            ...
    class D(C):
        def meth(self) -> int:
            ...
    

    现在我们知道 DC 的子类型,而 CP[float] 的子类型。但 D 不是 P[float] 的子类型,因为 D 实现了 P[int],而 P 是不变的。可以通过在 MRO 中查找协议实现来“解决”这个问题,但这在一般情况下过于复杂,并且这种“解决”方法需要放弃纯粹结构化协议子类型的简单概念。

  • 子类型检查将始终需要对协议进行类型推断。在上面的例子中,用户可能会抱怨:“为什么你为我的 D 推断出 P[int]?它实现了 P[float]!” 通常,推断可以通过显式注释来覆盖,但在这里这将需要显式子类化,这将违背使用协议的目的。
  • 允许覆盖方差将使类型检查器在引用成员类型签名中的特定冲突时,无法提供更详细的错误消息。
  • 最后,在这种情况下,显式比隐式更好。要求用户声明正确的方差将简化代码的理解,并避免在使用时出现意外错误。

支持适配器和适配

适配是由 PEP 246(被拒绝)提出的,并且由 zope.interface 支持,请参见 关于适配器注册表的 Zope 文档。适配器是一个相当高级的概念,而 PEP 484 支持可以替代适配器的联合类型和泛型别名。这可以通过 Iterable 协议的示例来说明,还有一种方法可以支持迭代,即提供 __getitem____len__。如果一个函数同时支持这种方法和现在标准的 __iter__ 方法,那么它可以用联合类型进行注释

class OldIterable(Sized, Protocol[T]):
    def __getitem__(self, item: int) -> T: ...

CompatIterable = Union[Iterable[T], OldIterable[T]]

class A:
    def __iter__(self) -> Iterator[str]: ...
class B:
    def __len__(self) -> int: ...
    def __getitem__(self, item: int) -> str: ...

def iterate(it: CompatIterable[str]) -> None:
    ...

iterate(A())  # OK
iterate(B())  # OK

由于对于这种情况,存在使用现有工具的合理替代方案,因此建议不要将适配器包含在这个 PEP 中。

将结构化基类型称为“接口”

“协议”是一个在 Python 中被广泛使用的术语,用于描述诸如迭代器协议(提供 __iter____next__)和描述符协议(提供 __get____set____delete__)等鸭子类型契约。除了这个原因和在 规范 中给出的其他原因之外,协议在几个方面不同于 Java 接口:协议不需要显式声明实现(它们主要面向鸭子类型),协议可以具有成员的默认实现并存储状态。

使协议在运行时成为特殊对象,而不是普通的 ABC

将协议设为非 ABC 将使向后兼容性成为问题,如果可能的话。例如,collections.abc.Iterable 已经是 ABC,并且许多现有代码使用类似于 isinstance(obj, collections.abc.Iterable) 的模式,以及其他 ABC 的类似检查(也是以结构化方式,即通过 __subclasshook__)。禁用这种行为将导致中断。如果我们对 collections.abc 中的 ABC 保留这种行为,但不会为 typing 中的协议提供类似的运行时行为,那么向协议的平滑过渡将不可能实现。此外,拥有两个并行层次结构可能会造成混淆。

向后兼容性

这个 PEP 具有完全向后兼容性。

实现

类型检查器 mypy 完全支持协议(一些已知错误除外)。这包括以结构化方式处理所有内置协议,例如 Iterable。协议的运行时实现可在 PyPI 上的 typing_extensions 模块中找到。

参考资料


来源: https://github.com/python/peps/blob/main/peps/pep-0544.rst

最后修改时间: 2024-06-11 22:12:09 GMT