我目前正在尝试Python 3.7中引入的新数据类构造。我目前坚持尝试做一些父类的继承。看来参数的顺序已被我当前的方法所破坏,因此子类中的bool参数在其他参数之前传递。这导致类型错误。

from dataclasses import dataclass

@dataclass
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')

@dataclass
class Child(Parent):
    school: str
    ugly: bool = True


jack = Parent('jack snr', 32, ugly=True)
jack_son = Child('jack jnr', 12, school = 'havard', ugly=True)

jack.print_id()
jack_son.print_id()

当我运行此代码时,我得到以下TypeError:
TypeError: non-default argument 'school' follows default argument

我该如何解决?

最佳答案

数据类组合属性的方式使您无法在基类中使用具有默认值的属性,然后在子类中使用没有默认值的属性(位置属性)。

这是因为通过从MRO的底部开始并按先见顺序建立属性的有序列表来组合属性。替代项将保留在其原始位置。因此,Parent['name', 'age', 'ugly']开头,其中ugly具有默认值,然后Child['school']添加到该列表的末尾(列表中已经存在ugly)。这意味着您最终会使用['name', 'age', 'ugly', 'school'],并且因为school没有默认值,这将导致__init__的参数列表无效。

这记录在PEP-557 Dataclasses下的inheritance中:



并在Specification下:



您确实有一些选择可以避免此问题。

第一种选择是使用单独的基类将具有默认值的字段强制置于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的反向MRO(忽略Child)是:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent

请注意,Parent不会设置任何新字段,因此此处以字段列出顺序中的“最后一个”结束并不重要。带有没有默认字段(_ParentBase_ChildBase)的类优先于带有默认字段(_ParentDefaultsBase_ChildDefaultsBase)的类。

结果是具有更老实字段的ParentChild类,而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'")

但这确实改变了场序; schoolugly之后结束:
<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>

并且类型提示检查器将提示_no_default不是字符串。

您还可以使用 attrs project,这是启发dataclasses的项目。它使用了不同的继承合并策略。它将子类中的重写字段拉到字段列表的末尾,因此['name', 'age', 'ugly']类中的Parent变为['name', 'age', 'school', 'ugly']类中的Child;通过使用默认值覆盖该字段,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

10-07 13:31
查看更多