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
和 Concrete2
从各自的超类继承了 method
。此行为记录在 PEP 的显式声明实现部分:
显式声明某个类实现给定的 协议,它可以用作常规基类。在这种情况下,有一个类 可以使用协议成员的默认实现。
...
请注意,显式和隐式之间几乎没有区别 子类型,显式子类化的主要好处是获得一些 协议方法“免费”。
但是,当协议类实现
__init__
方法时,__init__
不由协议类的显式子类继承。这与 ABC
类的子类形成对比,其中 do 继承了 __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
。
我的问题是:
__init__
背后的理由是什么?协议类无法“免费”提供 __init__
方法是否存在某种类型理论原因?您不能直接实例化协议类。这是当前实现的,通过将协议的
__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__
链。
这已在更高版本的 Python (
>=3.11
) 和 typing_extensions>=4.6.0
中得到修复。
根据评论
Python bug 跟踪器中的这个问题看起来相关:bugs.python.org/issue44807 ...因此我们可能会在 Python 版本 3.11 中看到对
/Protocol
交互的一些更新。__init__
Python 3.11确实解决了这个问题
Protocol 子类的
方法现在被保留。 (由 Adrian Garcia Badarasco 在 gh-88970 中贡献。)__init__()
对于 3.11 之前的版本,
typing_extensions>=4.6.0
package 也有修复。
在版本 4.6.0 中进行了更改:反向移植了在协议类上定义
方法的能力。__init__
使用来自
typing_extensions==4.12.2
的导入
>>> from abc import ABC
... from typing_extensions import Protocol # DIFFERENT IMPORT
...
... 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() # now prints "ProtocolBase.__init__ called"
AbstractBase.__init__ called
ProtocolBase.__init__ called