数据类组合属性的方式使您无法在基类中使用具有默认值的属性,然后在子类中使用没有默认值的属性(位置属性)。
这是因为通过从MRO的底部开始并按先见顺序建立属性的有序列表来组合属性。替代项将保留在其原始位置。因此,Parent
从开始['name', 'age', 'ugly']
,其中ugly
有一个默认值,然后Child
将其添加['school']
到该列表的末尾(ugly
已经在列表中)。这意味着您最终会['name', 'age', 'ugly', 'school']
因为且school
没有默认值而导致的参数列表无效__init__
。
这是记录在PEP-557数据类,在继承:
由@dataclass
装饰器创建数据类时,它将以反向MRO(即,从object
)开始遍历该类的所有基类,并针对找到的每个数据类,将该基类中的字段添加到有序对象中字段映射。添加所有基类字段后,它将自己的字段添加到有序映射中。所有生成的方法都将使用此组合的,经过计算的字段有序映射。由于字段按插入顺序排列,因此派生类将覆盖基类。
并在规格下:
TypeError
如果没有默认值的字段在具有默认值的字段之后,则会引发。当这发生在单个类中或作为类继承的结果时,都是如此。
您确实有一些选择可以避免此问题。
第一种选择是使用单独的基类将具有默认值的字段强制置于MRO顺序的更高位置。不惜一切代价,避免直接在将要用作基类的类上直接设置字段Parent
。
以下类层次结构有效:
@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
@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,该MRO会将所有没有默认值的字段放在具有默认值的字段之前。的反向MRO(忽略object
)为Child
:
_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)
另一种选择是仅使用具有默认值的字段。您仍然可以通过以下方法加一个错误,以致不提供school
值而产生错误__post_init__
:
_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
项目,它是启发灵感的项目dataclasses
。它使用了不同的继承合并策略。它拉在一子类中的字段列表的末尾重写字段,因此['name', 'age', 'ugly']
在Parent
类成为['name', 'age', 'school', 'ugly']
在Child
类; 通过使用默认值覆盖该字段,attrs
可以进行覆盖而无需进行MRO跳舞。
attrs
支持定义不带类型提示的字段,但是可以通过设置来坚持所支持的类型提示模式auto_attribs=True
:
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