PEP544——协议-结构化子类型(静态鸭子类型)

2023-10-29

  1. 摘要

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

2. 基本原理和目标

当前,PEP 484 和类型模块 [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]: ...

用户定义的抽象基类也会出现相同的问题:必须明确地将其子类化或注册。这对于库类型尤其困难,因为类型对象可能在库的实现中被深藏。此外,广泛使用抽象基类可能会带来额外的运行时成本。

此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 模块中的抽象基类已在运行时提供结构行为,isinstance(Bucket(), Iterable) 返回 True。该提议的主要目标是静态地支持这种行为。如下所述,将为用户定义的协议提供相同的功能。上面带有协议类的代码更好地匹配了通用的Python约定。它也可以自动扩展,并与恰好实现所需协议的其他不相关类一起使用。

2.1 名义子类型 vs 结构子类型

结构子类型对于Python程序员来说是很自然的,因为它与鸭子类型的运行时语义相匹配:具有某些属性的对象将独立于其实际运行时类进行处理。但是,正如 PEP 483 中所讨论的,名义子类型和结构子类型都有其优点和缺点。因此,在本PEP中,我们不建议将 PEP 484 描述的名义子类型完全替换为结构子类型。取而代之的是,此PEP中指定的协议类补充了常规类,用户可以自由选择将特定解决方案应用于何处。有关其他动机,请参阅本PEP末尾有关[拒绝的想法](# 8. 拒绝的想法)的部分。

2.2 非目标

在运行时,协议类将是简单的抽象基类。没有意图针对协议类提供复杂的运行时实例和类检查。这将是困难且容易出错的,并且将与 PEP 484 的逻辑相矛盾。同样,在 PEP 484PEP 526 之后,我们声明协议是完全可选的

  • 对于使用协议类注释的变量或参数,不会强加任何运行时语义。

  • 任何检查都只能由第三方类型检查器和其他工具执行。

  • 即使程序员使用类型注释,也可以自由使用它们。

  • 将来无意使协议成为非可选协议。

重申一下,为协议类提供复杂的运行时语义不是此PEP的目标,主要目标是为静态结构子类型提供支持和标准。在运行时上下文中将协议用作抽象基类的可能性相当小,主要是为已在使用抽象基类的项目提供无缝过渡。

3. 结构子类型的现有方法

在描述实际的规范之前,我们回顾并评论了与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")
    }
    

4. 规范

4.1 术语

我们建议将术语 协议 用于支持结构子类型化的类型。原因是,例如,术语 迭代器协议 在社区中得到了广泛理解,并且在静态类型的上下文中为此概念提出一个新术语只会造成混乱。

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

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

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

4.2 定义一个协议

通过在基类列表(通常在列表的末尾)中包含特殊的新类 typing.Protocolabc.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() 方法提供了支持。

4.3 协议成员

协议类主体中定义的所有方法都是协议成员,无论是常规成员还是使用 @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)。

4.4 显式声明实现

要显式声明某个类实现了给定的协议,可以将其用作常规基类。在这种情况下,一个类可以使用协议成员的默认实现。预期静态分析工具会自动检测类是否实现了给定的协议。因此,尽管可以显式地继承协议的子类,但出于类型检查的目的而不必这样做。

如果子类型关系是隐式的,并且仅通过结构子类型化,则不能使用默认实现——继承的语义不变。例如:

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 的语义没有改变,所有这些都必须由一个显式子类实现,然后才能实例化。

4.5 合并以及拓展协议

一般的哲学是协议大多类似于常规抽象基类,但是静态类型检查器将专门处理它们。协议类的子类不会将子类转换为协议,除非它也具有 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. 拒绝的想法)一节的内容。请注意,围绕显式子类化的规则与常规抽象基类有所不同,在常规抽象基类中,仅通过至少一种未实现的抽象方法即可简单地定义抽象性。协议类必须显式标记。

4.6 泛型协议

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

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

Protocol[T, S, ...] 允许被简写成 ProtocolGeneric[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.
        ...

4.7 递归协议

还支持递归协议。可以通过 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'

4.7 协议中的 Self-types

协议中的自类型 [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

4.8 回调协议

协议可用于定义灵活的回调类型,这些类型很难(甚至不可能)使用 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[...] 类型可以互换使用。

5. 使用协议

5.1 子类型与其他类型的关系

协议无法实例化,因此没有运行时类型为协议的值。对于具有协议类型的变量和参数,子类型关系遵循以下规则:

  • 协议绝不是具体类型的子类型。

  • 当且仅当 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  # 注意:ListLike 并未导入

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

populate([1, 2, 3])    # 通过类型检查
populate(MockStack())  # 也能通过类型检查

5.2 协议的并(Union)和交(intersection)

协议类的 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. 拒绝的想法)。

5.3 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

如果没有显式的类型,则可以将抽象基类或协议类分配给变量,并且这种分配会创建类型别名。对于普通(非抽象)类,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

5.4 NewType() 和 类型别名

协议本质上是匿名的。为了强调这一点,静态类型检查器可能会拒绝 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]]

5.5 模块作为协议的实现

如果给定模块的公共接口与期望的协议兼容,则在期望协议的地方接受模块对象。例如:

# 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

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

缺省语义是协议类型的 isinstance()issubclass() 检查将会失败。这是鸭子类型的精神——协议基本上将用于静态地对鸭子类型进行建模,而不是在运行时明确地进行建模。

但是,协议类型应该有可能在有意义时实施自定义实例和类检查,类似于 collection.abctyping 模块中的 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__') 的更简单方法。为了最大程度地降低此功能的风险,请遵循以下规则。

定义

  • 数据非数据协议:如果协议仅包含方法作为成员(例如,SizedIterator等),则该协议称为非数据协议。包含至少一个非方法成员的协议(如 x:int)称为数据协议
  • 不安全的重叠(Unsafe overlap):如果 X 不是 P 的子类型,但是它是 P 的擦除类型的子类型,其中所有成员都具有 Any 类型,则将 X 类型称为与协议 P 不安全地重叠。另外,如果联合的至少一个元素与协议 P 不安全地重叠,则整个联合与 P 不安全地重叠。

规范:

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

6. 在 Python2.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当前可用的反向端口反向移植到早期版本。

7. 协议类的运行时实现

7.1 实现细节

运行时实现可以在纯Python中完成,而对核心解释器和标准库中除过 typing 模块之外的其他模块没有任何影响,只需对 collections.abc 进行细微的更新:

  • 定义类 typing.protocol 类似于 typing.Generic
  • 实现功能以检测类是否为协议。如果一个类确实是协议,在添加一个类属性 _is_protocol = True。验证协议类在MRO中仅具有协议基类(object除外)。
  • 实现 @runtime_checkable,以允许 __subclasshook__() 执行结构实例和子类检查,就像在 collections.abc 类中所做的一样。
  • 所有结构子类型检查将由静态类型检查器执行,例如 mypy [mypy]。在运行时将不提供对协议验证的其他支持。

7.2 typing 模块中的更改

模块中的下列类将会成为协议:

  • Callable
  • Awaitable
  • Iterable, Iterator
  • AsyncIterable, AsyncIterator
  • Hashable
  • Sized
  • Container
  • Collection
  • Reversible
  • ContextManager, AsyncContextManager
  • SupportsAbs (以及其他的 Supports* 类)

这些类大多数都很小,概念上很简单。很容易看出这些协议实现了哪些方法,并立即识别出相应的运行时协议副本。实际上,由于其中一些类已经在运行时以必要的方式运行了,因此 typing 模块几乎不需要进行任何更改。其中大多数将仅需要在相应的 typeshed 存根 [typeshed] 中进行更新。

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

7.2 自省

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

如上所述,将仅对 isinstance()issubclass() 提供有限的支持(对于带下标的泛型协议,对这两个函数的调用总是会因 TypeError 而失败,因为在这种情况下无法在运行时给出可靠的答案)。但是,与其他自省工具一起,这可以为运行时类型检查工具提供合理的视角。

8. 拒绝的想法

请点击 这里 查看原文。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

PEP544——协议-结构化子类型(静态鸭子类型) 的相关文章

随机推荐

  • 【python】操作Excel和word模块

    一 Excel操作操作模块 xls和 xlsx使用的两种不同的模块 xls xlrd 常用函数 python操作excel主要用到xlrd和xlwt这两个库 即xlrd是读excel xlwt是写excel的库 data xlrd open
  • 混合双向无线传输装置助力智能工厂建设

    智能制造的出现 将为各制造企业解决现有问题 实现创新驱动转型提供了一条全新的发展思路和技术途径 智能工厂建设作为智能制造发展的载体 是当前制造业的发展愿景 也是正在积极努力的重点方向 但对于如何建设与企业战略规划一致且符合企业自身实际应用需
  • ES数据库学

    一 es数据库简介 1 简介 es数据库的英文全称为ElsticSearch 是位于Elastic Stack核心的分布式搜索和分析引擎 是一个由Apache开源的高扩展 全文检索和分析引擎 NoSQL数据库功能 的系统 它可以准实地快速存
  • ​5- GPIO操作

    本笔记是根据STM32F429单片机的 其他STM32单片机与此类似 基本上都一样的 会一款 其他的基本上没啥问题 本人也是刚学 有不足之处请下方评论 目录 笔记1 GPIO操作 1 1 读 修改 写操作 常用操作 位与 可实现目标字段的清
  • mysql 中show 命令用法详细介绍

    a show tables或show tables from database name 显示当前数据库中所有表的名称 b show databases 显示mysql中所有数据库的名称 c show columns from table
  • 专升本——倒装

    文章目录 1 动词的分类 2 完全倒装 3 部分倒装 3 1 前倒后不倒 4 也 的倒装 4 1 肯定 也 关键词 so 4 2 否定的 也 4 3 Only的倒装 4 3 1 补充 5 练习 6 As的倒装 7 练习 1 动词的分类 V
  • stm32学习笔记---GPIO口操作

    一 GPIO的基本概念及寄存器介绍 1 GPIO接口简介 通用输入输出接口GPIO是嵌入式系统 单片机开发过程中最常用的接口 用户可以通过编程灵活的对接口进行控制 实现对电路板上LED 数码管 按键等常用设备控制驱动 也可以作为串口的数据收
  • spring boot 处理请求的方式 Controller 与RestController

    spring boot 基于spring MVC的基础上进行了改进 将 Controller 与 ResponseBody 进行了合并成一个新的注解 RestController 当用户请求时 需要有视图渲染的 与请求数据的请求分别使用 C
  • 利用java工具实现发送Email

    目录 一 引言 二 封装建立相关session的工具类 三 发送电子邮件 一 引言 在电子邮件之前 人们发邮件通常是通过将信件投放到邮筒或交给邮递员然后经过交通运输到达收件人手里 电子邮件出现之后 类似的也是模仿这个原理 不过不是具体的纸质
  • 调试与优化

    文章目录 IDEA常用快捷键 单元测试JUnit 导包步骤 进行JUnit测试 IDEA常用快捷键 提示与代码生成部分 Alt Enter IDEA 根据光标所在问题 提供快速修复选择 Ctrl Alt T 对选中的代码弹出环绕选项弹出层
  • 毕业设计 单片机自动写字机器人 - 物联网 嵌入式 stm32

    文章目录 0 前言 1 简介 2 主要器件 3 实现效果 4 硬件设计 4 1 总体框架 4 2 AB32VG1主控MCU 5 软件说明 5 1 总体框架 6 部分核心代码 7 最后 0 前言 这两年开始毕业设计和毕业答辩的要求和难度不断提
  • java parser_java parser

    package org javaparser examples chapter2 import com github javaparser StaticJavaParser import com github javaparser ast
  • Seaborn常见绘图总结

    以前粗略的学习过Matplotlib绘图 Pandas绘图 这里是pandas的常见绘图总结 但是都未深入的去学习过 一遇到问题就翻文档 效率低下 听 他们 说matplotlib中的seaborn绘图很好看而且实用 所以 这里系统的总结一
  • web渗透测测试(sqlmap)

    通过分析靶机JLS02页面信息 寻找漏洞页面 将WEB服务存在SQL注入漏洞的页面名称作为Flag提交 Flag index2 php 通过本地PC中的渗透测试平台KALI2020对靶机进行SQL注入攻击 获取靶机的数据库信息 将用来存放W
  • JavaScript-----变量与数据类型

    目录 前言 1 注释 2 变量 定义变量 关键字 命名规范 3 数据类型 数据类型的判断 4 类型转换 前言 前面我们学过了C语言 Python Java之类的编程语言 所以我们有一定的基础的话学起来会比较轻松 同样的JavaScript也
  • hbase 知识点(1) hbase涉及维度

    1 数据模型 数据的存储方式 kv 非结构 2 存储模型 内存 磁盘 3 一致性 强一致性 4 物理模型 分布式 5 读写性能 同步写 副本支持读 6 辅助索引 key有效 bloom 7 故障管理 region移动 8 压缩 多种压缩算法
  • docker环境搭建实战--腾讯云课堂

    看完这篇文章 你会从一个docker小白变为一个不是那么菜的菜鸟 docker简介 docker是一个开放源代码软件项目 让应用程序部署软件的工作可以自动化进行 借此在linux操作系统上 提供一个额外的软件抽象层 以及操作系统层虚拟化的自
  • delphi操作ini文件

    INI文件操作 1 INI文件的结构 这是关于INI文件的注释部分 节点 关键字 值 INI文件允许有多个节点 每个节点又允许有多个关键字 后面是该关键字的值 类型有三种 字符串 整型数值和布尔值 其中字符串存贮在INI文件中时没有引号 布
  • J2SE-API:Object和System类的基本操作

    Object类 所有对象 包括数组 都实现这个类的方法 protected Object clone 创建并返回此对象的一个副本 boolean equals Object obj 指示其他某个对象是否与此对象 相等 protected v
  • PEP544——协议-结构化子类型(静态鸭子类型)

    文章目录 2 基本原理和目标 2 1 名义子类型 vs 结构子类型 2 2 非目标 3 结构子类型的现有方法 4 规范 4 1 术语 4 2 定义一个协议 4 3 协议成员 4 4 显式声明实现 4 5 合并以及拓展协议 4 6 泛型协议