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 484 中引入的类型提示可用于为静态类型检查器和其他第三方工具指定类型元数据。但是,PEP 484 仅指定*名义*子类型的语义。在本 PEP 中,我们指定协议类的静态和运行时语义,以支持*结构化*子类型(静态鸭子类型)。
基本原理和目标
目前,PEP 484 和 typing 模块 [typing] 为几个常见的 Python 协议(如 Iterable 和 Sized)定义了抽象基类。它们的问题在于,类必须明确标记才能支持它们,这不符合 Python 风格,也不像在惯用的动态类型 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 视为 Sized 和 Iterable[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 484 和 PEP 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.Protocol(abc.ABCMeta 的一个实例)来定义,通常在列表的末尾。这是一个简单的例子
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None:
...
现在,如果定义一个具有兼容签名的 close() 方法的类 Resource,它将隐式地成为 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
为了区分协议类变量和协议实例变量,应按照 PEP 526 的规定使用特殊的 ClassVar 注释。默认情况下,如上定义的协议变量被认为是可读写的。要定义只读协议变量,可以使用(抽象)属性。
显式声明实现
要明确声明某个类实现了给定协议,可以将其用作常规基类。在这种情况下,类可以使用协议成员的默认实现。静态分析工具应自动检测类是否实现了给定协议。因此,虽然可以显式子类化协议,但为了类型检查,*没有必要*这样做。
如果子类型关系是隐式的且仅通过结构化子类型,则不能使用默认实现——继承的语义不变。例如
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 中,抽象性简单地由至少一个抽象方法未实现来定义。协议类必须*显式*标记。
泛型协议
泛型协议很重要。例如,SupportsAbs、Iterable 和 Iterator 都是泛型协议。它们的定义类似于普通的非协议泛型类型
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'
协议中的自类型
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的所有协议成员,并且类型兼容。
泛型协议类型遵循与非协议类型相同的方差规则。协议类型可以在任何其他类型可以使用的所有上下文中,例如 Union、ClassVar、类型变量界限等。泛型协议遵循泛型抽象类的规则,除了使用结构兼容性而不是通过继承关系定义的兼容性。
静态类型检查器将识别协议实现,即使相应的协议*未导入*
# 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[] 与类对象 vs 协议
使用 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() 对于协议类型会失败。这符合鸭子类型的精神——协议基本上将用于静态地建模鸭子类型,而不是在运行时显式地建模。
但是,当有意义时,协议类型应该能够实现自定义实例和类检查,类似于 Iterable 以及 collections.abc 和 typing 中的其他 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)
请注意,实例检查在静态上并非 100% 可靠,这就是为什么这种行为是选择加入的,有关示例请参阅 被拒绝 的想法部分。类型检查器能做的最多就是将 isinstance(obj, Iterator) 大致视为编写 hasattr(x, '__iter__') and hasattr(x, '__next__') 的更简单方式。为了最大限度地降低此功能的风险,应用以下规则。
定义:
- 数据协议和非数据协议:如果一个协议只包含方法作为成员(例如
Sized、Iterator等),则称为非数据协议。如果一个协议包含至少一个非方法成员(如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 模块中的以下类将是协议
CallableAwaitableIterable,IteratorAsyncIterable,AsyncIteratorHashableSizedContainerCollectionReversibleContextManager,AsyncContextManagerSupportsAbs(和其他Supports*类)
这些类中的大多数都很小,概念上也很简单。很容易看出这些协议实现了哪些方法,并立即识别出相应的运行时协议对应物。实际上,typing 中需要进行的更改很少,因为其中一些类在运行时已经以必要的方式运行。其中大多数只需要在相应的 typeshed 存根 [typeshed] 中进行更新。
所有其他具体泛型类,例如 List、Set、IO、Deque 等,都足够复杂,将其保留为非协议(即要求代码明确声明它们)是有意义的。此外,意外遗漏某些方法太容易了,明确标记子类关系允许类型检查器精确指出缺失的实现。
内省
现有的类内省机制(dir、__annotations__ 等)可以与协议一起使用。此外,typing 模块中实现的所有内省工具都将支持协议。由于根据本提案,所有属性都需要在类体中定义,因此协议类在内省方面甚至比普通类有更好的前景,因为普通类中的属性可以隐式定义——协议属性不能以内省不可见的方式初始化(使用 setattr()、通过 self 赋值等)。尽管如此,在 Python 3.5 及更早版本中,某些内容(例如属性的类型)在运行时将不可见,但这似乎是一个合理的限制。
如上所述,isinstance() 和 issubclass() 的支持将受到限制(对于带有下标的泛型协议,这些检查*总是*会因 TypeError 而失败,因为在这种情况下无法在运行时给出可靠的答案)。但是,结合其他内省工具,这为运行时类型检查工具提供了合理的视角。
被拒绝/推迟的想法
本节中的想法之前已在 [several] [discussions] [elsewhere] 中讨论过。
默认将每个类都设为协议
一些语言,如 Go,将结构化子类型作为唯一或主要子类型形式。我们可以通过默认将所有类都设为协议(甚至总是如此)来实现类似的结果。然而,我们认为要求类显式标记为协议更好,原因如下
- 协议不具有常规类的一些属性。特别是,为普通类定义的
isinstance()基于名义层次结构。为了默认将所有内容都设为协议,并让isinstance()工作,需要更改其语义,这不会发生。 - 协议类通常不应有太多方法实现,因为它们描述的是接口,而不是实现。大多数类有许多方法实现,这使它们成为糟糕的协议类。
- 经验表明,无论如何,许多类作为协议并不实用,主要是因为它们的接口太大、太复杂或以实现为导向(例如,它们可能包含没有
__前缀的实际私有属性和方法)。 - 现有 Python 代码中大多数实际有用的协议似乎是隐式的。
typing和collections.abc中的 ABCs rather 是一个例外,但即使它们也是 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
现在,C 是 Proto 的子类型,Proto 是 Base 的子类型。但 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
然而,目前尚不清楚它会有多流行/有用,并且在类型检查器中为非协议类实现它可能会很困难。最后,如果需要,将来添加它会非常容易。
禁止非协议显式继承协议
这被拒绝了,原因如下
- 向后兼容性:人们已经在使用 ABCs,包括
typing模块中的泛型 ABCs。如果禁止显式子类化这些 ABCs,那么很多代码都会中断。 - 便利性:存在现有的类协议 ABC(可能转换为协议),它们具有许多有用的“混入”(非抽象)方法。例如,在
Sequence的情况下,只需在显式子类中实现__getitem__和__len__,即可免费获得__iter__、__contains__、__reversed__、index和count。 - 显式子类化明确表示一个类实现了特定的协议,使子类型关系更容易理解。
- 类型检查器可以更容易地警告缺失的协议成员或类型不兼容的成员,而无需使用本节前面讨论的虚拟赋值等技巧。
- 显式子类化使得强制将类视为协议的子类型(通过将
# 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: ...
现在我们有
D是C的子类型,C是P[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 中保留此行为,但不为 typing 中的协议提供类似的运行时行为,那么平稳过渡到协议将不可能。此外,拥有两个并行层次结构可能会造成混淆。
向后兼容性
本PEP完全向后兼容。
实施
mypy 类型检查器完全支持协议(除了少数已知错误)。这包括结构化处理所有内置协议,例如 Iterable。协议的运行时实现可在 PyPI 上的 typing_extensions 模块中获取。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0544.rst
上次修改时间: 2025-02-01 07:28:42 GMT