数据类组合属性的方式使您无法在基类中使用具有默认值的属性,然后在子类中使用没有默认值的属性(位置属性)。
这是因为属性是从 MRO 的底部开始组合的,并以先见的顺序构建属性的有序列表;覆盖保留在其原始位置。所以Parent 以['name', 'age', 'ugly'] 开头,其中ugly 有一个默认值,然后Child 将['school'] 添加到该列表的末尾(ugly 已经在列表中)。这意味着您最终会得到['name', 'age', 'ugly', 'school'],并且因为school 没有默认值,这会导致__init__ 的参数列表无效。
这记录在PEP-557 Dataclasses 中,在inheritance 下:
当 @dataclass 装饰器创建数据类时,它会在反向 MRO 中查看该类的所有基类(即从 object 开始),并且对于它找到的每个数据类,添加该基类中的字段到字段的有序映射。添加完所有基类字段后,它会将自己的字段添加到有序映射中。所有生成的方法都将使用这种组合的、计算的有序字段映射。因为字段是按插入顺序排列的,所以派生类会覆盖基类。
Specification以下:
TypeError 如果没有默认值的字段跟在有默认值的字段后面,则会引发。当这发生在单个类中或作为类继承的结果时,这是正确的。
您确实有几个选项可以避免此问题。
第一个选项是使用单独的基类将具有默认值的字段强制置于 MRO 顺序中的后面位置。不惜一切代价避免在要用作基类的类上直接设置字段,例如Parent。
以下类层次结构有效:
# base classes with fields; fields without defaults separate from fields with.
@dataclass
class _ParentBase:
name: str
age: int
@dataclass
class _ParentDefaultsBase:
ugly: bool = False
@dataclass
class _ChildBase(_ParentBase):
school: str
@dataclass
class _ChildDefaultsBase(_ParentDefaultsBase):
ugly: bool = True
# public classes, deriving from base-with, base-without field classes
# subclasses of public classes should put the public base class up front.
@dataclass
class Parent(_ParentDefaultsBase, _ParentBase):
def print_name(self):
print(self.name)
def print_age(self):
print(self.age)
def print_id(self):
print(f"The Name is {self.name} and {self.name} is {self.age} year old")
@dataclass
class Child(Parent, _ChildDefaultsBase, _ChildBase):
pass
通过将字段提取到具有不带默认值的字段和带默认值的字段的单独的基类中,以及精心选择的继承顺序,您可以生成一个 MRO,它将所有不带默认值的字段放在有默认值的字段之前。 Child 的反向 MRO(忽略 object)是:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
请注意,Parent 没有设置任何新字段,因此它在字段列表顺序中以“最后一个”结束并不重要。具有无默认字段的类(_ParentBase 和 _ChildBase)位于具有默认字段的类(_ParentDefaultsBase 和 _ChildDefaultsBase)之前。
结果是Parent 和Child 类具有较旧的健全字段,而Child 仍然是Parent 的子类:
>>> from inspect import signature
>>> signature(Parent)
<Signature (name: str, age: int, ugly: bool = False) -> None>
>>> signature(Child)
<Signature (name: str, age: int, school: str, ugly: bool = True) -> None>
>>> issubclass(Child, Parent)
True
因此您可以创建两个类的实例:
>>> jack = Parent('jack snr', 32, ugly=True)
>>> jack_son = Child('jack jnr', 12, school='havard', ugly=True)
>>> jack
Parent(name='jack snr', age=32, ugly=True)
>>> jack_son
Child(name='jack jnr', age=12, school='havard', ugly=True)
另一种选择是仅使用具有默认值的字段;您仍然可以通过在__post_init__ 中提高一个值来使不提供school 值的错误:
_no_default = object()
@dataclass
class Child(Parent):
school: str = _no_default
ugly: bool = True
def __post_init__(self):
if self.school is _no_default:
raise TypeError("__init__ missing 1 required argument: 'school'")
但这确实改变了字段顺序; school 在ugly 之后结束:
<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>
类型提示检查器会抱怨_no_default 不是字符串。
您也可以使用attrs project,这是启发dataclasses 的项目。它使用不同的继承合并策略;它将子类中被覆盖的字段拉到字段列表的末尾,因此Parent 类中的['name', 'age', 'ugly'] 变为Child 类中的['name', 'age', 'school', 'ugly'];通过使用默认值覆盖该字段,attrs 允许覆盖而无需进行 MRO 舞蹈。
attrs 支持在没有类型提示的情况下定义字段,但我们可以通过设置 auto_attribs=True 来坚持使用 supported type hinting mode:
import attr
@attr.s(auto_attribs=True)
class Parent:
name: str
age: int
ugly: bool = False
def print_name(self):
print(self.name)
def print_age(self):
print(self.age)
def print_id(self):
print(f"The Name is {self.name} and {self.name} is {self.age} year old")
@attr.s(auto_attribs=True)
class Child(Parent):
school: str
ugly: bool = True