数据类组合属性的方式阻止您在基类中使用带有默认值的属性,然后在子类中使用没有默认值的属性(位置属性)。
That's because the attributes are combined by starting from the bottom of the MRO, and building up an ordered list of the attributes in first-seen order; overrides are kept in their original location. So Parent starts out with ['name', 'age', 'ugly'], where ugly has a default, and then Child adds ['school'] to the end of that list (with ugly already in the list). This means you end up with ['name', 'age', 'ugly', 'school'] and because school doesn't have a default, this results in an invalid argument listing for __init__.
这被记录在PEP-557数据类中,在继承下:
When the Data Class is being created by the @dataclass decorator, it looks through all of the class's base classes in reverse MRO (that is, starting at object) and, for each Data Class that it finds, adds the fields from that base class to an ordered mapping of fields. After all of the base class fields are added, it adds its own fields to the ordered mapping. All of the generated methods will use this combined, calculated ordered mapping of fields. Because the fields are in insertion order, derived classes override base classes.
规格项下:
如果一个没有默认值的字段紧跟在一个有默认值的字段之后,将引发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(_ChildDefaultsBase, Parent, _ChildBase):
pass
通过将字段提取到具有无默认字段和具有默认字段的独立基类中,并仔细选择继承顺序,您可以生成一个MRO,将所有无默认字段放在具有默认字段之前。Child的反向MRO(忽略对象)是:
_ParentBase
_ChildBase
_ParentDefaultsBase
Parent
_ChildDefaultsBase
注意,虽然Parent没有设置任何新字段,但它确实从_ParentDefaultsBase继承了字段,并且不应该在字段列表顺序中以“最后”结束;上面的顺序把_ChildDefaultsBase放在最后,所以它的字段“win”。数据类规则也得到了满足;带默认字段的类(_ParentBase和_ChildBase)位于带默认字段的类(_ParentDefaultsBase和_ChildDefaultsBase)前面。
结果是父类和子类的字段都是旧的,而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__中引发一个错误来不提供学校值:
_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'")
但这确实改变了场的顺序;学校结束后丑陋:
<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>
类型提示检查器会提示_no_default不是字符串。
您还可以使用attrs项目,该项目激发了数据类的灵感。它使用了不同的继承合并策略;它将子类中被覆盖的字段拉到字段列表的末尾,因此父类中的['name', 'age', 'ugly']在子类中变成了['name', 'age', 'school', 'ugly'];通过使用默认值重写字段,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