数据类组合属性的方式使您无法在基类中使用具有默认值的属性,然后在子类中使用没有默认值的属性(位置属性)。
这是因为属性是从 MRO 的底部开始组合的,并以先见的顺序构建属性的有序列表;覆盖保留在其原始位置。所以Parent
从 开始['name', 'age', 'ugly']
,其中ugly
有一个默认值,然后Child
添加['school']
到该列表的末尾(ugly
已经在列表中)。这意味着您最终会得到['name', 'age', 'ugly', 'school']
并且因为school
没有默认值,这会导致__init__
.
这记录在PEP-557 Dataclasses中,在继承下:
当@dataclass
装饰器创建数据类时,它会在反向 MRO 中查看该类的所有基类(即从 开始object
),并且对于它找到的每个数据类,将该基类中的字段添加到有序字段映射。添加完所有基类字段后,它会将自己的字段添加到有序映射中。所有生成的方法都将使用这种组合的、计算的有序字段映射。因为字段是按插入顺序排列的,所以派生类会覆盖基类。
并在规范下:
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,将所有没有默认值的字段放在有默认值的字段之前。反向 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