首页 > 解决方案 > 在显式子类型构造期间未调用 typing.Protocol 类 `__init__` 方法

问题描述

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"

正如预期的那样,具体的子类Concrete1继承Concrete2method它们各自的超类。此行为记录在 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

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

我的问题是:

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

标签: python-3.xprotocolspython-typingabstract-base-class

解决方案


您不能直接实例化协议类。目前这是通过将协议替换为__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

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

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


推荐阅读