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

问题描述 投票:0回答:2

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

我的问题是:

  1. 不让协议类的显式子类型继承
    __init__
    背后的理由是什么?协议类无法“免费”提供
    __init__
    方法是否存在某种类型理论原因?
  2. 有关于这种差异的任何文件吗?还是bug?
python protocols python-typing abstract-base-class
2个回答
16
投票

您不能直接实例化协议类。这是当前实现的,通过将协议的

__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__
链。


0
投票

总结

这已在更高版本的 Python (

>=3.11
) 和
typing_extensions>=4.6.0
中得到修复。

背景

根据评论

Python bug 跟踪器中的这个问题看起来相关:bugs.python.org/issue44807 ...因此我们可能会在 Python 版本 3.11 中看到对

Protocol
/
__init__
交互的一些更新。

Python 3.11确实解决了这个问题

Protocol 子类的

__init__()
方法现在被保留。 (由 Adrian Garcia Badarasco 在 gh-88970 中贡献。)

对于 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
© www.soinside.com 2019 - 2024. All rights reserved.