165

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

我该如何解决?

4

14 回答 14

219

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

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

这记录在PEP-557 Dataclasses中,在继承下:

@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(忽略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
于 2018-10-31T14:38:11.540 回答
48

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

甚至

@dataclass
class Child(Parent):
    school: str
    ugly = True
    # This does not work
    # ugly: bool = True

jack_son = Child('jack jnr', 12, school = 'havard')
assert jack_son.ugly
于 2019-10-23T14:57:25.980 回答
19

请注意,在Python 3.10中,现在可以使用数据类在本机上执行此操作。

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

直接取自Eric Smith 关于该主题的博客文章,这是人们要求此功能的两个原因:

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

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

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)

请查看上面链接的博客文章,以获得对 kw_only 的更全面的解释。

干杯!

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

于 2021-11-03T09:37:27.143 回答
14

下面的方法在使用纯 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()

如果你想使用一个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()

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

于 2019-04-22T15:39:51.900 回答
10

您看到此错误是因为在具有默认值的参数之后添加了没有默认值的参数。继承字段插入数据类的顺序与方法解析顺序相反,这意味着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,在 class 中指定C

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

于 2018-07-30T13:55:28.910 回答
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
于 2019-02-12T15:16:33.383 回答
2

您可以使用修改后的数据类版本,它将生成仅关键字__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的,没有丑陋的黑客。

于 2020-01-29T17:49:12.373 回答
2

一种可能的解决方法是使用猴子补丁来附加父字段

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, ...)

于 2019-09-27T08:36:12.037 回答
2

在发现数据类可能正在获取允许重新排序字段的装饰器参数后,我回到了这个问题。这无疑是一个有希望的发展,尽管此功能的进展似乎有些停滞。

现在,您可以通过使用dataclassy获得这种行为以及其他一些细节,这是我对克服此类挫折的数据类的重新实现。在原始示例中使用from dataclassy代替from dataclasses意味着它可以正常运行。

使用检查来打印签名Child可以清楚地了解发生了什么;结果是(name: str, age: int, school: str, ugly: bool = True)。字段总是被重新排序,以便在初始化器的参数中具有默认值的字段位于没有它们的字段之后。两个列表(没有默认值的字段和有默认值的字段)仍然按定义顺序排序。

面对这个问题是促使我编写数据类替代品的因素之一。此处详述的解决方法虽然很有帮助,但需要将代码扭曲到这样的程度,以至于它们完全否定了数据类的幼稚方法(因此字段排序很容易预测)提供的可读性优势。

于 2020-08-02T20:05:49.407 回答
2

一个快速而肮脏的解决方案:

from typing import Optional

@dataclass
class Child(Parent):
    school: Optional[str] = None
    ugly: bool = True

    def __post_init__(self):
        assert self.school is not None

然后返回并重构一次(希望)语言被扩展。

于 2021-02-13T15:18:43.877 回答
2

在使用 Python 继承创建数据类时,不能保证所有具有默认值的字段都会出现在所有没有默认值的字段之后。

一个简单的解决方案是避免使用多重继承来构造“合并”数据类。相反,我们可以通过对父数据类的字段进行过滤和排序来构建一个合并的数据类。

试试这个merge_dataclasses()功能:

import dataclasses
import functools
from typing import Iterable, Type


def merge_dataclasses(
    cls_name: str,
    *,
    merge_from: Iterable[Type],
    **kwargs,
):
    """
    Construct a dataclass by merging the fields
    from an arbitrary number of dataclasses.

    Args:
        cls_name: The name of the constructed dataclass.

        merge_from: An iterable of dataclasses
            whose fields should be merged.

        **kwargs: Keyword arguments are passed to
            :py:func:`dataclasses.make_dataclass`.

    Returns:
        Returns a new dataclass
    """
    # Merge the fields from the dataclasses,
    # with field names from later dataclasses overwriting
    # any conflicting predecessor field names.
    each_base_fields = [d.__dataclass_fields__ for d in merge_from]
    merged_fields = functools.reduce(
        lambda x, y: {**x, **y}, each_base_fields
    )

    # We have to reorder all of the fields from all of the dataclasses
    # so that *all* of the fields without defaults appear
    # in the merged dataclass *before* all of the fields with defaults.
    fields_without_defaults = [
        (f.name, f.type, f)
        for f in merged_fields.values()
        if isinstance(f.default, dataclasses._MISSING_TYPE)
    ]
    fields_with_defaults = [
        (f.name, f.type, f)
        for f in merged_fields.values()
        if not isinstance(f.default, dataclasses._MISSING_TYPE)
    ]
    fields = [*fields_without_defaults, *fields_with_defaults]

    return dataclasses.make_dataclass(
        cls_name=cls_name,
        fields=fields,
        **kwargs,
    )

然后您可以按如下方式合并数据类。请注意,我们可以合并AB默认字段b,并将d其移动到合并数据类的末尾。

@dataclasses.dataclass
class A:
    a: int
    b: int = 0


@dataclasses.dataclass
class B:
    c: int
    d: int = 0


C = merge_dataclasses(
    "C",
    merge_from=[A, B],
)

# Note that 
print(C(a=1, d=1).__dict__)
# {'a': 1, 'd': 1, 'b': 0, 'c': 0}

当然,这个解决方案的缺陷是它C实际上并没有继承Aand B,这意味着你不能使用isinstance()or 其他类型的断言来验证 C 的父系。

于 2021-08-27T00:45:25.307 回答
1

一个实验性但有趣的解决方案是使用元类。下面的解决方案允许使用具有简单继承的 Python 数据类,而dataclass根本不使用装饰器。此外,它可以继承父基类的字段,而无需抱怨位置参数(非默认字段)的顺序。

from collections import OrderedDict
import typing as ty
import dataclasses
from itertools import takewhile

class DataClassTerm:
    def __new__(cls, *args, **kwargs):
        return super().__new__(cls)

class DataClassMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        fields = {}

        # Get list of base classes including the class to be produced(initialized without its original base classes as those have already become dataclasses)
        bases_and_self = [dataclasses.dataclass(super().__new__(cls, clsname, (DataClassTerm,), clsdict))] + list(bases)

        # Whatever is a subclass of DataClassTerm will become a DataClassTerm. 
        # Following block will iterate and create individual dataclasses and collect their fields
        for base in bases_and_self[::-1]: # Ensure that last fields in last base is prioritized
            if issubclass(base, DataClassTerm):
                to_dc_bases = list(takewhile(lambda c: c is not DataClassTerm, base.__mro__))
                for dc_base in to_dc_bases[::-1]: # Ensure that last fields in last base in MRO is prioritized(same as in dataclasses)
                    if dataclasses.is_dataclass(dc_base):
                        valid_dc = dc_base
                    else:
                        valid_dc = dataclasses.dataclass(dc_base)
                    for field in dataclasses.fields(valid_dc):
                        fields[field.name] = (field.name, field.type, field)
        
        # Following block will reorder the fields so that fields without default values are first in order
        reordered_fields = OrderedDict()
        for n, t, f  in fields.values():
            if f.default is dataclasses.MISSING and f.default_factory is dataclasses.MISSING:
                reordered_fields[n] = (n, t, f)
        for n, t, f  in fields.values():
            if n not in reordered_fields.keys():
                reordered_fields[n] = (n, t, f)
        
        # Create a new dataclass using `dataclasses.make_dataclass`, which ultimately calls type.__new__, which is the same as super().__new__ in our case
        fields = list(reordered_fields.values())
        full_dc = dataclasses.make_dataclass(cls_name=clsname, fields=fields, init=True, bases=(DataClassTerm,))
        
        # Discard the created dataclass class and create new one using super but preserve the dataclass specific namespace.
        return super().__new__(cls, clsname, bases, {**full_dc.__dict__,**clsdict})
    
class DataClassCustom(DataClassTerm, metaclass=DataClassMeta):
    def __new__(cls, *args, **kwargs):
        if len(args)>0:
            raise RuntimeError("Do not use positional arguments for initialization.")
        return super().__new__(cls, *args, **kwargs)

现在让我们创建一个带有父数据类和示例混合类的示例数据类:

class DataClassCustomA(DataClassCustom):
    field_A_1: int = dataclasses.field()
    field_A_2: ty.AnyStr = dataclasses.field(default=None)

class SomeOtherClass:
    def methodA(self):
        print('print from SomeOtherClass().methodA')

class DataClassCustomB(DataClassCustomA,SomeOtherClass):
    field_B_1: int = dataclasses.field()
    field_B_2: ty.Dict = dataclasses.field(default_factory=dict)

结果是

result_b = DataClassCustomB(field_A_1=1, field_B_1=2)

result_b
# DataClassCustomB(field_A_1=1, field_B_1=2, field_A_2=None, field_B_2={})

result_b.methodA()
# print from SomeOtherClass().methodA

尝试@dataclass对每个父类上的装饰器执行相同操作会在以下子类中引发异常,例如TypeError(f'non-default argument <field-name) follows default argument'). 上述解决方案可以防止这种情况发生,因为首先对字段进行了重新排序。但是,由于修改了字段的顺序,因此必须防止*args使用 in DataClassCustom.__new__,因为原始顺序不再有效。

尽管在 Python >=3.10kw_only中引入了本质上使数据类中的继承更加可靠的特性,但上面的示例仍然可以用作使数据类可继承的一种方法,不需要使用@dataclass装饰器。

于 2022-02-20T19:21:39.117 回答
1

如何定义这样的ugly字段,而不是默认方式?

ugly: bool = field(metadata=dict(required=False, missing=False))
于 2021-08-26T16:20:51.130 回答
1

补充使用attrs的 Martijn Pieters 解决方案:可以在没有默认属性复制的情况下创建继承,其中:

import attr

@attr.s(auto_attribs=True)
class Parent:
    name: str
    age: int
    ugly: bool = attr.ib(default=False, kw_only=True)


@attr.s(auto_attribs=True)
class Child(Parent):
    school: str
    ugly: bool = True

更多关于kw_only参数的信息可以在这里找到

于 2021-06-18T19:41:02.743 回答