PEP 484 中引入的类型提示可用于为静态类型检查器和其他第三方工具指定类型元数据。但是,PEP 484 只指定了名义子类型(nominal subtyping)的语义。在这个PEP中,我们指定了协议类的静态和运行时语义,这些协议类将为结构子类型(structural subtyping),即 静态鸭子类型 提供支持。
当前,PEP 484 和类型模块 [typing] 为几种常见的Python协议(例如 Iterable
和 Sized
)定义了抽象基类。它们的问题在于必须显式标记一个类以支持它们,这样是不够 Pythonic 的,并且与惯用的动态类型化的Python代码通常不一样。例如,下面的示例符合PEP 484:
from typing import Sized, Iterable, Iterator
class Bucket(Sized, Iterable[int]):
...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[int]: ...
用户定义的抽象基类也会出现相同的问题:必须明确地将其子类化或注册。这对于库类型尤其困难,因为类型对象可能在库的实现中被深藏。此外,广泛使用抽象基类可能会带来额外的运行时成本。
此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
模块中的抽象基类已在运行时提供结构行为,isinstance(Bucket(), Iterable)
返回 True
。该提议的主要目标是静态地支持这种行为。如下所述,将为用户定义的协议提供相同的功能。上面带有协议类的代码更好地匹配了通用的Python约定。它也可以自动扩展,并与恰好实现所需协议的其他不相关类一起使用。
结构子类型对于Python程序员来说是很自然的,因为它与鸭子类型的运行时语义相匹配:具有某些属性的对象将独立于其实际运行时类进行处理。但是,正如 PEP 483 中所讨论的,名义子类型和结构子类型都有其优点和缺点。因此,在本PEP中,我们不建议将 PEP 484 描述的名义子类型完全替换为结构子类型。取而代之的是,此PEP中指定的协议类补充了常规类,用户可以自由选择将特定解决方案应用于何处。有关其他动机,请参阅本PEP末尾有关[拒绝的想法](# 8. 拒绝的想法)的部分。
在运行时,协议类将是简单的抽象基类。没有意图针对协议类提供复杂的运行时实例和类检查。这将是困难且容易出错的,并且将与 PEP 484 的逻辑相矛盾。同样,在 PEP 484 和 PEP 526 之后,我们声明协议是完全可选的:
对于使用协议类注释的变量或参数,不会强加任何运行时语义。
任何检查都只能由第三方类型检查器和其他工具执行。
即使程序员使用类型注释,也可以自由使用它们。
将来无意使协议成为非可选协议。
重申一下,为协议类提供复杂的运行时语义不是此PEP的目标,主要目标是为静态结构子类型提供支持和标准。在运行时上下文中将协议用作抽象基类的可能性相当小,主要是为已在使用抽象基类的项目提供无缝过渡。
在描述实际的规范之前,我们回顾并评论了与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)
正如在 [基本原理](# 2. 基本原理和目标) 中所提到的,我们希望避免这种必要性,特别是在静态环境中。然而,在运行时上下文中,抽象基类是协议类的良好候选,它们已经在类型模块中广泛使用。
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禁止实现中的冗余成员。虽然可选成员的想法看起来很有趣,但它会使这个提议复杂化,而且不清楚它会有多有用。因此,提议将其推迟;参见[拒绝的想法](# 8. 拒绝的想法)。一般来说,没有运行时影响的静态协议检查的想法看起来是合理的,基本上这个建议遵循了相同的路线。
Go [golang] 使用了一种更激进的方法,将接口作为提供类型信息的主要方式。此外,赋值(assignments)被用来明确地确保实现:
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
注解。默认情况下,以上定义的协议变量被视为可读写。要定义只读协议变量,可以使用(抽象)特性(abstract property)。
要显式声明某个类实现了给定的协议,可以将其用作常规基类。在这种情况下,一个类可以使用协议成员的默认实现。预期静态分析工具会自动检测类是否实现了给定的协议。因此,尽管可以显式地继承协议的子类,但出于类型检查的目的而不必这样做。
如果子类型关系是隐式的,并且仅通过结构子类型化,则不能使用默认实现——继承的语义不变。例如:
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
的语义没有改变,所有这些都必须由一个显式子类实现,然后才能实例化。
一般的哲学是协议大多类似于常规抽象基类,但是静态类型检查器将专门处理它们。协议类的子类不会将子类转换为协议,除非它也具有 typing.Protocol
作为显式基类。没有这个基类,该类将被 “降级” 为常规抽象基类,不能与结构子类型一起使用。该规则的基本原理是,我们不希望仅仅因为其基类之一恰好是一个类而意外地将某些类用作协议。在静态类型世界中,我们仍然稍微喜欢名义子类型而不是结构子类型。
子协议可以通过将一个或多个协议作为直接基类并具有 typing.Protocol
作为直接基类来定义:
from typing import Sized, Protocol
class SizedAndClosable(Sized, Protocol):
def close(self) -> None:
...
现在,协议 SizedAndClosable
是具有 __len__
和 close
这两种方法的协议。如果在基类列表中省略了Protocol
,则它将是必须实现 Sized
的常规(非协议)类。或者,可以通过将[定义](# 4.2 定义一个协议)一节中的示例中的 SupportsClose
协议与typing.Sized
合并来实现 SizedAndClosable
协议。
from typing import Sized
class SupportsClose(Protocol):
def close(self) -> None:
...
class SizedAndClosable(Sized, SupportsClose, Protocol):
pass
SizedAndClosable
的两个定义是等效的。考虑子类型化时,协议之间的子类关系没有意义,因为结构兼容性是标准,而不是MRO。
如果 Protocol
包含在基类列表中,则所有其他基类必须是协议。协议不能扩展常规类,请参阅被[拒绝的想法](# 8. 拒绝的想法)一节的内容。请注意,围绕显式子类化的规则与常规抽象基类有所不同,在常规抽象基类中,仅通过至少一种未实现的抽象方法即可简单地定义抽象性。协议类必须显式标记。
泛型协议很重要。例如,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]'.
请注意,与名义类不同,事实上的协变协议不能声明为不变式,因为这会破坏子类型的可传递性(有关详细信息,请参阅[拒绝的想法](# 8. 拒绝的想法)一节)。例如:
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'
协议中的自类型 [self-types] 遵循 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
的子类型。
泛型协议类型遵循与非协议类型相同的差异规则。协议类型可以在可以使用任何其他类型的所有上下文中使用,例如在 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 # 注意:ListLike 并未导入
class MockStack:
def __len__(self) -> int:
return 42
def append(self, x: int) -> None:
print(x)
populate([1, 2, 3]) # 通过类型检查
populate(MockStack()) # 也能通过类型检查
协议类的 Union
与非协议类的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 的规定添加特殊的交叉类型构造(intersection type construct),有关更多详细信息,请参见[拒绝的想法](# 8. 拒绝的想法)。
用 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
如果没有显式的类型,则可以将抽象基类或协议类分配给变量,并且这种分配会创建类型别名。对于普通(非抽象)类,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 # 类型检查错误,签名不匹配。
b: ProtoB = C # OK
协议本质上是匿名的。为了强调这一点,静态类型检查器可能会拒绝 NewType()
中的协议类,以避免产生提供不同类型的错觉:
from typing import NewType, Protocol, Iterator
class Id(Protocol):
code: int
secrets: Iterator[bytes]
UserId = NewType('UserId', Id) # 错误,不能提供不同的类型
相反,完全支持类型别名,包括通用类型别名:
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
缺省语义是协议类型的 isinstance()
和 issubclass()
检查将会失败。这是鸭子类型的精神——协议基本上将用于静态地对鸭子类型进行建模,而不是在运行时明确地进行建模。
但是,协议类型应该有可能在有意义时实施自定义实例和类检查,类似于 collection.abc
和 typing
模块中的 Iterable
和其他抽象基类已经实现的功能,但这仅限于非泛型和未下标的泛型协议(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__')
和 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()
调用之后,类型检查器应该能够从联合中选择正确的元素。为了缩小非联合类型的类型,类型检查器可以使用其最佳判断(这是有意未指定的,因为精确的规范需要交叉(intersection)类型)。在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中仅具有协议基类(object
除外)。@runtime_checkable
,以允许 __subclasshook__()
执行结构实例和子类检查,就像在 collections.abc
类中所做的一样。mypy
[mypy]。在运行时将不提供对协议验证的其他支持。模块中的下列类将会成为协议:
Callable
Awaitable
Iterable
, Iterator
AsyncIterable
, AsyncIterator
Hashable
Sized
Container
Collection
Reversible
ContextManager
, AsyncContextManager
SupportsAbs
(以及其他的 Supports*
类)这些类大多数都很小,概念上很简单。很容易看出这些协议实现了哪些方法,并立即识别出相应的运行时协议副本。实际上,由于其中一些类已经在运行时以必要的方式运行了,因此 typing
模块几乎不需要进行任何更改。其中大多数将仅需要在相应的 typeshed
存根 [typeshed] 中进行更新。
所有其他具体的泛型类(例如 List
,Set
,IO
,Deque
等)都非常复杂,因此有必要将它们保持为非协议(即要求代码对其进行明确声明)。另外,很容易使某些方法意外地未实现,并且显式标记子类关系允许类型检查器查明缺少的实现。
现有的类自省机制(dir
,__annotations__
等)可以与协议一起使用。此外,在 typing
模块中实现的所有自省工具都将支持协议。由于需要根据此建议在类主体中定义所有属性,因此协议类比常规类具有更好的自省视角,常规类可以隐式定义属性——协议属性不能以自省不可见的方式初始化(使用 setattr()
,通过 self
进行赋值等)。尽管如此,某些属性之类的东西在Python 3.5及更早版本的运行时仍不可见,但这似乎是一个合理的限制。
如上所述,将仅对 isinstance()
和 issubclass()
提供有限的支持(对于带下标的泛型协议,对这两个函数的调用总是会因 TypeError
而失败,因为在这种情况下无法在运行时给出可靠的答案)。但是,与其他自省工具一起,这可以为运行时类型检查工具提供合理的视角。
请点击 这里 查看原文。