Python 3.7数据类中的类继承

Mys*_*rio 35 python python-3.x python-3.7 python-dataclasses

我目前正在尝试使用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()
Run Code Online (Sandbox Code Playgroud)

当我运行此代码时,我得到了这个TypeError:

TypeError: non-default argument 'school' follows default argument
Run Code Online (Sandbox Code Playgroud)

我该如何解决?

小智 121

请注意,使用Python 3.10,现在可以使用数据类本地完成此操作。

Dataclasses 3.10 添加了kw_only属性(类似于attrs)。它允许您指定哪些字段是keyword_only,因此将在init的末尾设置,不会导致继承问题。

直接摘自埃里克·史密斯关于该主题的博客文章

人们[要求]此功能有两个原因:

  • 当数据类具有许多字段时,按位置指定它们可能会变得不可读。它还要求为了向后兼容,所有新字段都添加到数据类的末尾。这并不总是令人满意的。
  • 当一个数据类从另一个数据类继承时,并且基类具有带默认值的字段,则派生类中的所有字段也必须具有默认值。

下面是使用这个新参数的最简单方法,但是您可以通过多种方式使用它来在父类中使用带有默认值的继承:

from dataclasses import dataclass

@dataclass(kw_only=True)
class Parent:
    name: str
    age: int
    ugly: bool = False

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

ch = Child(name="Kevin", age=17, school="42")
print(ch.ugly)
Run Code Online (Sandbox Code Playgroud)

请查看上面链接的博文,了解 kw_only 的更全面解释。

干杯!

PS:由于它相当新,请注意您的 IDE 可能仍会引发错误,但它可以在运行时运行

  • 请注意,您还可以在特定字段上使用“kw_only=True”。在上面的示例中,您可以编写“ugly: bool = field(default=False, kw_only=True)”,这将使“ugly”字段仅成为关键字,而将其他字段保留为必填字段。 (8认同)

Mar*_*ers 45

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

那是因为属性是从MRO的底部开始组合的,并按照首先看到的顺序构建属性的有序列表; 覆盖保留在原始位置.所以Parent从一开始['name', 'age', 'ugly'],ugly默认情况下,然后Child添加['school']到该列表的末尾(ugly已经在列表中).这意味着您最终得到['name', 'age', 'ugly', 'school']并且因为school没有默认值,这会导致无效的参数列表__init__.

这在PEP-557数据类中有记录,在继承下:

@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
Run Code Online (Sandbox Code Playgroud)

通过将字段拉出到单独的基类中,其中包含没有默认值的字段和具有默认值的字段,以及精心选择的继承顺序,您可以生成一个MRO,在没有默认值的情况下将所有字段放在默认值之前.反向MRO(忽略object)Child是:

_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
Run Code Online (Sandbox Code Playgroud)

请注意,Parent这不会设置任何新字段,因此它在字段列表顺序中以"last"结尾并不重要.具有无默认值(_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
Run Code Online (Sandbox Code Playgroud)

所以你可以创建这两个类的实例:

>>> 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)
Run Code Online (Sandbox Code Playgroud)

另一种选择是仅使用具有默认值的字段; 你仍然可以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'")
Run Code Online (Sandbox Code Playgroud)

但这确实改变了野外秩序; school结束后ugly:

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

并且类型提示检查器抱怨_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
Run Code Online (Sandbox Code Playgroud)


小智 25

如果从 init 函数中排除它们,则可以在父类中使用具有默认值的属性。如果您需要在初始化时覆盖默认值的可能性,请使用 Praveen Kulkarni 的答案扩展代码。

from dataclasses import dataclass, field

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = field(default=False, init=False)

@dataclass
class Child(Parent):
    school: str

jack = Parent('jack snr', 32)
jack_son = Child('jack jnr', 12, school = 'havard')
jack_son.ugly = True
Run Code Online (Sandbox Code Playgroud)

  • 我想这个答案应该得到更多的认可。它解决了父类中存在默认字段的问题,从而消除了 TypeError。 (2认同)

Pra*_*rni 10

下面的方法在使用纯 pythondataclasses并且没有太多样板代码的情况下处理这个问题。

ugly_init: dataclasses.InitVar[bool]充当伪场正好可以帮助我们做初始化,一旦被创建的实例将丢失。Whileugly: bool = field(init=False)是一个实例成员,它不会通过__init__方法初始化,但可以使用__post_init__方法进行初始化(您可以在此处找到更多信息。)。

from dataclasses import dataclass, field

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = field(init=False)
    ugly_init: dataclasses.InitVar[bool]

    def __post_init__(self, ugly_init: bool):
        self.ugly = ugly_init

    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

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

jack.print_id()
jack_son.print_id()
Run Code Online (Sandbox Code Playgroud)

如果要使用ugly_init可选的模式,可以在 Parent 上定义一个类方法,该方法包含ugly_init一个可选参数:

from dataclasses import dataclass, field, InitVar

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = field(init=False)
    ugly_init: InitVar[bool]

    def __post_init__(self, ugly_init: bool):
        self.ugly = ugly_init
    
    @classmethod
    def create(cls, ugly_init=True, **kwargs):
        return cls(ugly_init=ugly_init, **kwargs)

    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

jack = Parent.create(name='jack snr', age=32, ugly_init=False)
jack_son = Child.create(name='jack jnr', age=12, school='harvard')

jack.print_id()
jack_son.print_id()

Run Code Online (Sandbox Code Playgroud)

现在您可以使用create类方法作为工厂方法来创建具有默认值的父/子类ugly_init。请注意,您必须使用命名参数才能使用此方法。


Pat*_*ugh 7

您看到此错误,因为在具有默认值的参数之后添加了没有默认值的参数.继承字段到数据类的插入顺序与方法解析顺序相反,这意味着Parent字段首先出现,即使它们的子节点稍后写入.

PEP-557的一个例子- 数据类:

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

@dataclass
class C(Base):
    z: int = 10
    x: int = 15
Run Code Online (Sandbox Code Playgroud)

最终的字段列表按顺序排列x, y, z.最终类型xint,如类中所指定C.

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

  • 不幸的是,我认为没有办法解决它。我的理解是,如果父类有默认参数,那么任何子类都不能有非默认参数。 (4认同)

Dan*_*ral 5

基于 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}'"
                )
Run Code Online (Sandbox Code Playgroud)

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

from src.utils import no_default, NoDefaultAttributesChild

@dataclass
class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin):
    attr1: str = no_default
Run Code Online (Sandbox Code Playgroud)

编辑:

一段时间后,我也发现此解决方案与 mypy 存在问题,以下代码解决了该问题。

from dataclasses import dataclass
from typing import TypeVar, Generic, Union

T = TypeVar("T")


class NoDefault(Generic[T]):
    ...


NoDefaultVar = Union[NoDefault[T], T]
no_default: NoDefault = NoDefault()


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


@dataclass
class Parent(NoDefaultAttributesPostInitMixin):
    a: str = ""

@dataclass
class Child(Foo):
    b: NoDefaultVar[str] = no_default
Run Code Online (Sandbox Code Playgroud)