为了制作在 mypy 下进行类型检查的抽象数据类,我将它们分为两个类,一类包含抽象方法,一类包含数据成员,如这个答案中所述。抽象类继承自数据类。但是,当另一个抽象类和数据类对从第一个抽象类和数据类对继承时,这会遇到问题:“祖先”数据类的字段被“后代”擦除。例如:
from dataclasses import dataclass
from abc import ABC, abstractmethod
@dataclass
class ADataclassMixin:
a_field: int = 1
class A(ADataclassMixin, ABC):
@abstractmethod
def method(self):
pass
@dataclass
#class BDataclassMixin(A): # works but fails mypy 0.931 type-check
class BDataclassMixin: # fails
b_field: int = 2
pass
class B(BDataclassMixin, A):
def method(self):
return self
o = B(a_field=5)
最后一行失败,产生此错误消息:
TypeError: BDataclassMixin.__init__() got an unexpected keyword argument 'a_field'
正如预期的那样,B 的方法解析顺序 (
B.__mro__
) 是 (B, BDataclassMixin, A, ADataclassMixin, ABC, object)
。但是找不到a_field
。
如上面注释掉的行所示,一个解决方案是将祖先类显式放在后代数据类的声明中:
class BDataclassMixin(A)
而不是class BDataclassMixin
。但这会导致类型检查失败,因为数据类只能是具体类。
如果我们添加第三个类(继承自 B),上述解决方案就会崩溃:
@dataclass
#class CDataclassMixin: # fails
class CDataclassMixin(A): # fails
#class CDataclassMixin(B, A): # works but fails type-check
c_field: int = 3
pass
class C(CDataclassMixin, B):
def method(self):
return "C's result"
pass
o = C(b_field=5)
现在,C 拥有
a_field
和 c_field
,但失去了 b_field
。
我发现,如果我显式声明 CDataclassMixin 从 B 和 A 继承(按顺序),
b_field
将与 a_field_ and
c_field` 一起出现在结果类中。然而,在每个混入中明确声明继承层次结构违背了混入的目的,即能够独立于所有其他混入对它们进行编码,并以您喜欢的任何方式轻松地混合它们。
制作抽象数据类混合的正确方法是什么,以便从它们继承的类包含所有数据类字段?
正确的解决方案是放弃 DataclassMixin 类,而简单地将抽象类变成数据类,如下所示:
@dataclass # type: ignore[misc]
class A(ABC):
a_field: int = 1
@abstractmethod
def method(self):
pass
@dataclass # type: ignore[misc]
class B(A):
b_field: int = 2
@dataclass
class C(B):
c_field: int = 3
def method(self):
return self
失败的原因是,正如有关数据类的文档中所述,数据类中的完整字段集是在数据类编译时确定的,而不是在继承数据类时确定。生成数据类的 __init__
函数的内部代码只能检查数据类本身声明的 MRO,而不能在混合到另一个类中时检查。有必要将
# type: ignore[misc]
添加到每个抽象数据类的
@dataclass
行,不是因为解决方案是错误的,而是因为mypy 是错误的。需要数据类具体的是 mypy,,而不是 Python。正如 ilevkivskyi 在 mypy 第 5374 期 中所解释的那样,问题是 mypy 希望数据类成为
Type
对象并且 每个
Type
对象都能够被实例化。这是一个已知问题,等待解决。问题和解决方案中的行为正是数据类应该如何表现。而且,令人高兴的是,以这种方式(普通方式)继承的抽象数据类可以随意混合到其他类中,与其他混合没有什么不同。
@dataclass
class ADataclassMixin:
a_field: int = 1
class A(ABC, ADataclassMixin):
@abstractmethod
def method(self):
pass
@dataclass
class BDataclassMixin:
b_field: int = 2
class B(A, BDataclassMixin):
def method(self):
return self
o = B(a_field=5)
print((o.a_field, o.b_field)) # (5,2)