Python 3.7 数据类中的类继承

2025-03-18 08:54:00
admin
原创
57
摘要:问题描述:我目前正在尝试使用 Python 3.7 中引入的新数据类构造。我目前无法尝试继承父类。看起来我当前的方法搞乱了参数的顺序,以至于子类中的 bool 参数在其他参数之前传递。这会导致类型错误。from dataclasses import dataclass @dataclass class Pa...

问题描述:

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

我该如何修复这个问题?


解决方案 1:

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

这是因为属性是从 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(_ChildDefaultsBase, Parent, _ChildBase):
    pass

通过将字段提取到具有无默认值字段和具有默认值字段的单独基类中,并仔细选择继承顺序,您可以生成一个 MRO,将所有没有默认值的字段放在具有默认值的字段之前。 的反向 MRO(忽略objectChild是:

_ParentBase
_ChildBase
_ParentDefaultsBase
Parent
_ChildDefaultsBase

请注意,虽然Parent没有设置任何新字段,但它确实继承了字段,_ParentDefaultsBase并且应该在字段列表顺序中排在“最后”;上面的顺序排在_ChildDefaultsBase最后,所以它的字段“获胜”。数据类规则也得到满足;没有默认值的字段的类(_ParentBase_ChildBase)位于具有默认值的字段的类(_ParentDefaultsBase_ChildDefaultsBase)之前。

结果是ParentChild具有合理字段 older 的类,而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

解决方案 2:

请注意,使用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 可能仍会引发可能的错误,但它在运行时有效

解决方案 3:

如果将父类中的属性从 init 函数中排除,则可以使用这些属性的默认值。如果您需要在 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

    def __post_init__(self):
       object.__setattr__(self, 'ugly', True)

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

甚至

@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

解决方案 4:

下面的方法使用纯 Python 来解决这个问题dataclasses,而且不需要太多的样板代码。

用作伪字段ugly: dataclasses.InitVar[bool],仅用于帮助我们进行初始化,实例创建后将丢失。而是实例成员,不会通过方法初始化,但可以使用方法进行初始化(您可以在此处找到更多信息)。_ugly: bool = field(init=False)`__init__`__post_init__

from dataclasses import dataclass, field, InitVar

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

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

    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=True)
jack_son = Child('jack jnr', 12, school='havard', ugly=True)

jack.print_id()
jack_son.print_id()

请注意,这使得字段ugly变为必需的,您可以在 Parent 上定义一个类方法,其中包含ugly一个可选参数:

from dataclasses import dataclass, field, InitVar

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

    def __post_init__(self, ugly: bool):
        self._ugly = ugly
    
    @classmethod
    def create(cls, ugly=True, **kwargs):
        return cls(ugly=ugly, **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=False)
jack_son = Child.create(name='jack jnr', age=12, school='harvard')

jack.print_id()
jack_son.print_id()

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

解决方案 5:

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

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

解决方案 6:

根据 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

解决方案 7:

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

现在,您可以使用dataclassy获得这种行为以及其他一些优点,这是我对数据类的重新实现,可以克服此类挫折。在原始示例中使用from dataclassy代替意味着它运行时不会出错。from dataclasses

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

面对这个问题是促使我编写数据类替代品的因素之一。这里详述的解决方法虽然有用,但需要对代码进行一定程度的扭曲,以至于完全抵消了数据类的简单方法(即字段排序很容易预测)所提供的可读性优势。

解决方案 8:

如果您使用的是 Python 3.10+,那么您可以使用数据类的仅关键字参数,正如本答案和Python 文档中所述。

如果您使用的是 < Python 3.10,那么您可以使用dataclasses.field抛出default_factory的。由于该属性将用 声明field(),因此它会被视为具有默认值;但如果用户尝试创建实例而不提供该字段的值,它将使用工厂,这将出错。

此技术并不等同于仅使用关键字,因为您仍然可以按位置提供所有参数。但是,这确实解决了问题,并且比摆弄各种数据类 dunder 方法更简单。

from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, TypeVar

T = TypeVar("T")


def required() -> T:
    f: T

    def factory() -> T:
        # mypy treats a Field as a T, even though it has attributes like .name, .default, etc
        field_name = f.name  # type: ignore[attr-defined]
        raise ValueError(f"field '{field_name}' required")

    f = field(default_factory=factory)
    return f


@dataclass
class Event:
    id: str
    created_at: datetime
    updated_at: Optional[datetime] = None


@dataclass
class NamedEvent(Event):
    name: str = required()


event = NamedEvent(name="Some Event", id="ab13c1a", created_at=datetime.now())
print("created event:", event)


event2 = NamedEvent("ab13c1a", datetime.now(), name="Some Other Event")
print("created event:", event2)

event3 = NamedEvent("ab13c1a", datetime.now())

输出:

created event: NamedEvent(id='ab13c1a', created_at=datetime.datetime(2022, 7, 23, 19, 22, 17, 944550), updated_at=None, name='Some Event')
created event: NamedEvent(id='ab13c1a', created_at=datetime.datetime(2022, 7, 23, 19, 22, 17, 944588), updated_at=None, name='Some Other Event')
Traceback (most recent call last):
  File ".../gist.py", line 39, in <module>
    event3 = NamedEvent("ab13c1a", datetime.now())
  File "<string>", line 6, in __init__
  File ".../gist.py", line 14, in factory
    raise ValueError(f"field '{field_name}' required")
ValueError: field 'name' required

您还可以在这个 github gist上找到此代码。

解决方案 9:

Python 3.10 数据类具有 kw_only=True 属性,可消除此问题。以下操作无错误:

from dataclasses import dataclass

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

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

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

jack.print_id()
jack_son.print_id()

请注意,实例化类时必须指定所有字段的参数名称。

解决方案 10:

一种可能的解决方法是使用 monkey-patching 来附加父字段

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,但这可能太脏了。

虽然 monkey-patching 缺少一些继承特性,但它仍然可以用于向所有伪子类添加方法。

为了进行更细粒度的控制,请使用设置默认值dc.field(compare=False, repr=True, ...)

解决方案 11:

您可以使用数据类的修改版本,它将生成仅关键字的__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 反向移植进行了测试)

这将需要将子类定义为

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

并会生成__init__(self, *, name:str, age:int, ugly:bool=True, school:str)(这是有效的 Python)。这里唯一的警告是不允许使用位置参数初始化对象,但除此之外,它完全是常规的,dataclass没有丑陋的黑客。

解决方案 12:

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

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

然后一旦语言得到扩展(希望如此),就回去重构。

解决方案 13:

当您使用 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实际上并没有从和继承,这意味着您不能使用或其他类型断言来验证 C 的父级。A`B`isinstance()

解决方案 14:

补充使用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参数的更多信息可以在这里找到

解决方案 15:

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

ugly: bool = field(metadata=dict(required=False, missing=False))

解决方案 16:

一个实验性但有趣的解决方案是使用元类。下面的解决方案允许使用具有简单继承的 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使用,DataClassCustom.__new__因为原始顺序不再有效。

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

解决方案 17:

装饰器@dataclass有选项init=False、文档:

init:如果为 true(默认值),__init__则会生成一个 () 方法。如果类已经定义了init (),则忽略此参数。

对于这个例子,我们可以使用选项来设置自定义的、老式的(和无聊的)__init__构造函数,但然后我们可以根据需要设置顺序和默认值(适用于 python 3.7+):

from dataclasses import dataclass

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = False

@dataclass(init=False)
class Child(Parent):
    school: str
    ugly: bool = True
    new_id: int

    def __init__(self, name: str, age: int, school: str, 
                 new_id:int, ugly: bool = True):
        self.name, self.age, self.school, self.new_id, \n             self.ugly = name, age, school, new_id, ugly

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

print(jack)
# -> Parent(name='jack snr', age=32, ugly=True)
print(jack_son)
# -> Child(name='jack jnr', age=12, ugly=True, school='havard', 
#          new_id=12)

由于__init__未生成方法,因此必须注意:

  • 数据类字段默认值(即default)将被忽略 - 您必须在方法声明default_factory中(重新)定义默认值__init__()

  • __init__方法 +init=False只能在子类中定义/设置,父类可以保持原样

  • InitVar并且ClassVar类似的数据类类型提示被忽略

  • 数据类字段的顺序规则“第一个没有默认值的字段 -> 然后有默认值的字段”不适用,你可以做任何你喜欢的事情 - 检查new_id示例(尽管默认值无论如何都会被忽略,请参阅第一条注释)

总结一下,dataclass 生成方法非常方便和安全__init__,所以当实在没有更好的选择时,请使用这种自定义__init__kw_only=True技术。个人而言,我更喜欢有解决方案的解决方案。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2482  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1533  
  PLM(产品生命周期管理)项目对于企业优化产品研发流程、提升产品质量以及增强市场竞争力具有至关重要的意义。然而,在项目推进过程中,范围蔓延是一个常见且棘手的问题,它可能导致项目进度延迟、成本超支以及质量下降等一系列不良后果。因此,有效避免PLM项目范围蔓延成为项目成功的关键因素之一。以下将详细阐述三大管控策略,助力企业...
plm系统   0  
  PLM(产品生命周期管理)项目管理在企业产品研发与管理过程中扮演着至关重要的角色。随着市场竞争的加剧和产品复杂度的提升,PLM项目面临着诸多风险。准确量化风险优先级并采取有效措施应对,是确保项目成功的关键。五维评估矩阵作为一种有效的风险评估工具,能帮助项目管理者全面、系统地评估风险,为决策提供有力支持。五维评估矩阵概述...
免费plm软件   0  
  引言PLM(产品生命周期管理)开发流程对于企业产品的全生命周期管控至关重要。它涵盖了从产品概念设计到退役的各个阶段,直接影响着产品质量、开发周期以及企业的市场竞争力。在当今快速发展的科技环境下,客户对产品质量的要求日益提高,市场竞争也愈发激烈,这就使得优化PLM开发流程成为企业的必然选择。缺陷管理工具和六西格玛方法作为...
plm产品全生命周期管理   0  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用