在显式子类型构造期间未调用typing.Protocol 类`__init__` 方法

Jas*_*sha 3 protocols abstract-base-class python-3.x python-typing

Python 的PEP 544引入typing.Protocol了结构子类型,也就是“静态鸭子类型”。

在此 PEP 关于合并和扩展协议的部分中,指出

一般的哲学是协议大多像常规的 ABC,但静态类型检查器会专门处理它们。

因此,人们期望从 的子类继承与期望从 的子类继承typing.Protocol的方式大致相同abc.ABC

from abc import ABC
from typing import Protocol

class AbstractBase(ABC):
    def method(self):
        print("AbstractBase.method called")

class Concrete1(AbstractBase):
    ...

c1 = Concrete1()
c1.method()  # prints "AbstractBase.method called"

class ProtocolBase(Protocol):
    def method(self):
        print("ProtocolBase.method called")

class Concrete2(ProtocolBase):
    ...

c2 = Concrete2()
c2.method()  # prints "ProtocolBase.method called"
Run Code Online (Sandbox Code Playgroud)

正如所料,具体子类Concrete1Concrete2继承method从他们各自的超类。此行为记录在PEP的显式声明实现部分:

要明确声明某个类实现了给定的协议,可以将其用作常规基类。在这种情况下,类可以使用协议成员的默认实现。

...

请注意,显式和隐式子类型之间几乎没有区别,显式子类化的主要好处是“免费”获得一些协议方法。

然而,当协议类实现__init__的方法,__init__通过协议类的明确的子类继承。这是相对于一个的子ABC类,继承__init__方法:

from abc import ABC
from typing import Protocol

class AbstractBase(ABC):
    def __init__(self):
        print("AbstractBase.__init__ called")

class Concrete1(AbstractBase):
    ...

c1 = Concrete1()  # prints "AbstractBase.__init__ called"

class ProtocolBase(Protocol):
    def __init__(self):
        print("ProtocolBase.__init__ called")

class Concrete2(ProtocolBase):
    ...

c2 = Concrete2()  # NOTHING GETS PRINTED
Run Code Online (Sandbox Code Playgroud)

我们看到,Concrete1继承__init__AbstractBase,但Concrete2不继承__init__ProtocolBase。这与前面的示例形成对比,其中Concrete1Concrete2method从各自的超类继承。

我的问题是:

  1. 没有__init__被协议类的显式子类型继承的基本原理是什么?协议类无法__init__“免费”提供方法是否存在某种类型理论原因?
  2. 有没有关于这种差异的文件?或者这是一个错误?

use*_*ica 6

您不能直接实例化协议类。这目前是通过用一种__init__方法替换协议来实现的,该方法的唯一功能是强制执行此限制:

def _no_init(self, *args, **kwargs):
    if type(self)._is_protocol:
        raise TypeError('Protocols cannot be instantiated')

...

class Protocol(Generic, metaclass=_ProtocolMeta):
    ...

    def __init_subclass__(cls, *args, **kwargs):
        ...
        cls.__init__ = _no_init
Run Code Online (Sandbox Code Playgroud)

__init__不会执行,因为它不再存在。

这很奇怪,而且比乍一看的东西还要多——例如,它与多重继承的交互很差,中断super().__init__链。