Python 3.7数据类中的类继承

2024-05-23 15:56:20 发布

您现在位置:Python中文网/ 问答频道 /正文

我目前正在尝试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

我该怎么解决?


Tags: nameselfidtrueage参数defclass
3条回答

基于Martijn Pieters的解决方案,我做了以下工作:

1)创建一个实现post_init的混合

from dataclasses import dataclass

no_default = object()


@dataclass
class NoDefaultAttributesPostInitMixin:

    def __post_init__(self):
        for key, value in self.__dict__.items():
            if value is no_default:
                raise TypeError(
                    f"__init__ missing 1 required argument: '{key}'"
                )

2)然后在存在继承问题的类中:

from src.utils import no_default, NoDefaultAttributesChild

@dataclass
class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin):
    attr1: str = no_default

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

这是因为属性是从MRO的底部开始组合的,并按照第一个看到的顺序构建属性的有序列表;覆盖保留在其原始位置。所以Parent['name', 'age', 'ugly']开始,其中ugly有默认值,然后Child['school']添加到列表的末尾(列表中已经有ugly)。这意味着您以['name', 'age', 'ugly', 'school']结束,并且由于school没有默认值,这将导致__init__的参数列表无效。

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

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.

Specification下:

TypeError will be raised if a field without a default value follows a field with a default value. This is true either when this occurs in a single class, or as a result of class inheritance.

你在这里有一些选择来避免这个问题。

第一个选项是使用单独的基类将具有默认值的字段强制放到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

通过将字段拖出到separate基类(具有不带默认值的字段和具有默认值的字段)中,并按照精心选择的继承顺序,可以生成一个MRO,将所有不带默认值的字段放在具有默认值的字段之前。Child的反向MRO(忽略object)是:

_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'")

但是这个确实改变了字段顺序;school在^{之后结束:

<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>

类型提示检查器会抱怨_no_default不是字符串。

您还可以使用^{} 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

您看到此错误是因为在具有默认值的参数之后添加了一个没有默认值的参数。继承字段插入数据类的顺序与Method Resolution Order相反,这意味着Parent字段排在第一位,即使它们稍后被其子级重写。

来自PEP-557 - Data Classes的示例:

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

The final list of fields is, in order,x, y, z. The final type of x is int, as specified in class C.

不幸的是,我认为没有办法解决这个问题。我的理解是,如果父类有默认参数,那么子类就不能有非默认参数。

相关问题 更多 >