Python 3.7 数据类中的类继承
- 2025-03-18 08:54:00
- admin 原创
- 55
问题描述:
我目前正在尝试使用 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(忽略object
)Child
是:
_ParentBase
_ChildBase
_ParentDefaultsBase
Parent
_ChildDefaultsBase
请注意,虽然Parent
没有设置任何新字段,但它确实继承了字段,_ParentDefaultsBase
并且不应该在字段列表顺序中排在“最后”;上面的顺序排在_ChildDefaultsBase
最后,所以它的字段“获胜”。数据类规则也得到满足;没有默认值的字段的类(_ParentBase
和_ChildBase
)位于具有默认值的字段的类(_ParentDefaultsBase
和_ChildDefaultsBase
)之前。
结果是Parent
和Child
具有合理字段 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
。 的最终类型x
为int
,如类 中所述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,
)
然后,您可以按如下方式合并数据类。请注意,我们可以合并A
和B
默认字段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
技术。个人而言,我更喜欢有解决方案的解决方案。
扫码咨询,免费领取项目管理大礼包!