Python 3.7数据类中的类继承


84

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

我该如何解决?

Answers:


123

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

这是因为通过从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

通过将字段拉到具有默认值的字段和具有默认值的字段以及精心选择的继承顺序的单独的基类中,可以生成MRO,该MRO会将所有没有默认值的字段放在具有默认值的字段之前。的反向MRO(忽略object)为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'")

但这确实改变了场序;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

1
非常感谢您提供详细的答案
Mysterio '18

这非常有帮助。我对mro感到困惑。运行打印(Child.mro())获得:[<类的.Child'>,<类的.Parent'>,<类的._ChildDefaultsBase'>,<类的._ParentDefaultsBase'>,<类的._ChildBase'>,<类的._ParentBase'>,<类“对象”>]所以,不要默认碱基先于基类?
奥利

1
@Ollie,这是正确的顺序;请注意,我在答案中列出了它。当您有多个基类时,您需要一种方法来线性化所涉及的类,以决定继承时哪些类先于其他类。Python使用C3线性化方法,我的答案利用了它的工作原理,以确保具有默认值的属性始终排在所有没有默认值的属性之后。
马丁·彼得斯

实际上,attrs可以工作,但是您需要使用attr.ib(kw_only=True),请参阅github.com/python-attrs/attrs/issues/38
laike9m

8

您会看到此错误,因为在具有默认值的参数之后添加了没有默认值的参数。继承的字段在数据类中的插入顺序与“方法解析顺序”方法相反,这意味着Parent即使字段后来被其子项覆盖,这些字段还是排在最前面。

PEP-557的一个示例-数据类

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

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

最终的字段列表是x, y, z。的最终类型xint,在类中指定C

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


我知道非默认参数必须在默认参数之前,但是当父参数在添加子参数之前初始化时如何处理呢?
Mysterio '18

3
不幸的是,我认为没有任何解决方法。我的理解是,如果父类具有默认参数,那么子类都不能具有非默认参数。
Patrick Haugh

1
您可以在我标记该信息之前将其添加到答案中吗?有一天会有帮助。不幸的是,数据类的局限性。将其渲染为我当前的python项目。很高兴看到这样的实现方式
Mysterio '18

5

如果将属性从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

我认为这个答案应该得到更多的认可。它解决了在父类中具有默认字段的问题,从而消除了TypeError。
尼尔斯·本格森

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

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

from src.utils import no_default, NoDefaultAttributesChild

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

编辑:

一段时间后,我也发现使用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

您是否打算在2)中编写“ class MyDataclass(DataclassWithDefaults,NoDefaultAttributesPostInitMixin)”?
Scott P.

4

以下方法在使用纯pythondataclasses且没有太多样板代码的情况下解决了此问题。

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

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()

ugly_init现在是必需参数,没有默认值
Vadym Tyemirov

2

在发现数据类可能正在获取允许对字段进行重新排序的decorator参数之后,我回到了这个问题。尽管此功能的开发似乎已停滞不前,但这无疑是一个有希望的发展。

现在,通过使用dataclassy,我可以重新实现数据类,从而克服这种挫败感,从而获得这种行为以及一些其他好处。from dataclassyfrom dataclasses原始示例中使用代替意味着它可以正常运行。

使用检查打印出的签名Child使发生的事情清晰;结果是(name: str, age: int, school: str, ugly: bool = True)。字段总是被重新排序,以便具有默认值的字段在初始化器的参数中没有字段的后面。两个列表(没有默认值的字段以及带有默认值的字段)仍按定义顺序排序。

与这个问题面对面是促使我编写数据类替代品的因素之一。此处详细介绍的变通办法虽然很有帮助,但要求将代码扭曲到一定程度,以至于它们完全否定了可读性优势,即数据类的朴素方法(从而可以轻易地预测字段顺序)。


1

可能的解决方法是使用猴子修补程序来附加父字段

import dataclasses as dc

def add_args(parent): 
    def decorator(orig):
        "Append parent's fields AFTER orig's fields"

        # Aggregate fields
        ff  = [(f.name, f.type, f) for f in dc.fields(dc.dataclass(orig))]
        ff += [(f.name, f.type, f) for f in dc.fields(dc.dataclass(parent))]

        new = dc.make_dataclass(orig.__name__, ff)
        new.__doc__ = orig.__doc__

        return new
    return decorator

class Animal:
    age: int = 0 

@add_args(Animal)
class Dog:
    name: str
    noise: str = "Woof!"

@add_args(Animal)
class Bird:
    name: str
    can_fly: bool = True

Dog("Dusty", 2)               # --> Dog(name='Dusty', noise=2, age=0)
b = Bird("Donald", False, 40) # --> Bird(name='Donald', can_fly=False, age=40)

也可以通过选中,在非默认字段前添加前缀if f.default is dc.MISSING,但这可能太脏了。

尽管猴子修补缺少继承的某些功能,但仍可以将其用于向所有伪子类添加方法。

要进行更细粒度的控制,请使用 dc.field(compare=False, repr=True, ...)


1

您可以使用数据类的修改版本,这将生成仅关键字__init__方法:

import dataclasses


def _init_fn(fields, frozen, has_post_init, self_name):
    # fields contains both real fields and InitVar pseudo-fields.
    globals = {'MISSING': dataclasses.MISSING,
               '_HAS_DEFAULT_FACTORY': dataclasses._HAS_DEFAULT_FACTORY}

    body_lines = []
    for f in fields:
        line = dataclasses._field_init(f, frozen, globals, self_name)
        # line is None means that this field doesn't require
        # initialization (it's a pseudo-field).  Just skip it.
        if line:
            body_lines.append(line)

    # Does this class have a post-init function?
    if has_post_init:
        params_str = ','.join(f.name for f in fields
                              if f._field_type is dataclasses._FIELD_INITVAR)
        body_lines.append(f'{self_name}.{dataclasses._POST_INIT_NAME}({params_str})')

    # If no body lines, use 'pass'.
    if not body_lines:
        body_lines = ['pass']

    locals = {f'_type_{f.name}': f.type for f in fields}
    return dataclasses._create_fn('__init__',
                      [self_name, '*'] + [dataclasses._init_param(f) for f in fields if f.init],
                      body_lines,
                      locals=locals,
                      globals=globals,
                      return_type=None)


def add_init(cls, frozen):
    fields = getattr(cls, dataclasses._FIELDS)

    # Does this class have a post-init function?
    has_post_init = hasattr(cls, dataclasses._POST_INIT_NAME)

    # Include InitVars and regular fields (so, not ClassVars).
    flds = [f for f in fields.values()
            if f._field_type in (dataclasses._FIELD, dataclasses._FIELD_INITVAR)]
    dataclasses._set_new_attribute(cls, '__init__',
                       _init_fn(flds,
                                frozen,
                                has_post_init,
                                # The name to use for the "self"
                                # param in __init__.  Use "self"
                                # if possible.
                                '__dataclass_self__' if 'self' in fields
                                else 'self',
                                ))

    return cls


# a dataclass with a constructor that only takes keyword arguments
def dataclass_keyword_only(_cls=None, *, repr=True, eq=True, order=False,
              unsafe_hash=False, frozen=False):
    def wrap(cls):
        cls = dataclasses.dataclass(
            cls, init=False, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)
        return add_init(cls, frozen)

    # See if we're being called as @dataclass or @dataclass().
    if _cls is None:
        # We're called with parens.
        return wrap

    # We're called as @dataclass without parens.
    return wrap(_cls)

(也作为要点发布,通过Python 3.6 backport测试)

这将需要将子类定义为

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

并会生成__init__(self, *, name:str, age:int, ugly:bool=True, school:str)(有效的python)。这里唯一的警告是不允许使用位置参数初始化对象,但是否则这是完全正常的dataclass,没有难看的骇客。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.