Наследование классов в классах данных Python 3.7
В настоящее время я пробую свои силы на новых конструкциях класса данных, представленных в 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
Как это исправить?
16 ответов
Вы видите эту ошибку, потому что после аргумента со значением по умолчанию добавляется аргумент без значения по умолчанию. Порядок вставки унаследованных полей в класс данных является обратным порядку разрешения метода, что означает, что 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
,
К сожалению, я не думаю, что есть способ обойти это. Насколько я понимаю, если родительский класс имеет аргумент по умолчанию, то ни один дочерний класс не может иметь аргументы не по умолчанию.
То, как классы данных комбинируют атрибуты, не позволяет вам использовать атрибуты со значениями по умолчанию в базовом классе, а затем использовать атрибуты без значений по умолчанию (позиционные атрибуты) в подклассе.
Это связано с тем, что атрибуты объединяются, начиная с нижней части 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 (игнорируя object
) за Child
является:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
Обратите внимание, что Parent
не устанавливает никаких новых полей, поэтому не имеет значения, что он заканчивается 'последним' в порядке перечисления полей. Классы с полями без значений по умолчанию (_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
и поэтому вы можете создавать экземпляры обоих классов:
>>> 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
Обратите внимание, что с Python 3.10 теперь можно делать это изначально с помощью классов данных.
Dataclasses 3.10 добавил
kw_only
атрибут (аналогично attrs). Он позволяет вам указать, какие поля являются ключевыми словами, поэтому они будут установлены в конце инициализации, не вызывая проблемы наследования.
Исходя непосредственно из сообщения в блоге Эрика Смита на эту тему , люди просили об этой функции по двум причинам:
- Если в классе данных много полей, их определение по позиции может стать нечитаемым. Также требуется, чтобы для обратной совместимости все новые поля добавлялись в конец класса данных. Это не всегда желательно.
- Когда класс данных наследуется от другого класса данных, а базовый класс имеет поля со значениями по умолчанию, тогда все поля в производном классе также должны иметь значения по умолчанию.
Далее следует самый простой способ сделать это с помощью этого нового аргумента, но есть несколько способов его использования для использования наследования со значениями по умолчанию в родительском классе:
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 может по-прежнему вызывать возможную ошибку, но она работает во время выполнения.
Вы можете использовать атрибуты со значениями по умолчанию в родительских классах, если исключите их из функции init. Если вам нужна возможность переопределить значение по умолчанию в init, дополните код ответом Правина Кулкарни.
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
Приведенный ниже подход решает эту проблему при использовании чистого Python. dataclasses
и без особого шаблонного кода.
В ugly_init: dataclasses.InitVar[bool]
служит псевдополем, чтобы помочь нам выполнить инициализацию, и будет потеряно после создания экземпляра. Покаugly: 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()
На основе решения 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, NoDefaultAttributesChild):
attr1: str = no_default
Если вы используете 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 .
Классы данных 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()
Обратите внимание, что при создании экземпляра класса вам необходимо указать имена аргументов для всех полей.
Быстрое и грязное решение:
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
Затем вернитесь и выполните рефакторинг, когда (надеюсь) язык будет расширен.
Возможный обходной путь - использовать исправление обезьяны для добавления родительских полей
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, ...)
Вы можете использовать модифицированную версию классов данных, которая будет генерировать только ключевое слово __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)
(это действительный питон). Единственное предостережение здесь - не разрешать инициализировать объекты позиционными аргументами, но в остальном это совершенно обычныйdataclass
без уродливых хаков.
Я вернулся к этому вопросу после того, как обнаружил, что классы данных могут получать параметр декоратора, который позволяет изменять порядок полей. Это, безусловно, многообещающая разработка, хотя разработка этой функции, похоже, несколько застопорилась.
Прямо сейчас вы можете получить это поведение, а также некоторые другие тонкости, используя dataclassy, мою повторную реализацию классов данных, которая преодолевает подобные разочарования. С помощьюfrom dataclassy
на месте from dataclasses
в исходном примере означает, что он работает без ошибок.
Использование inspect для печати подписиChild
проясняет происходящее; результат(name: str, age: int, school: str, ugly: bool = True)
. Поля всегда переупорядочиваются, так что поля со значениями по умолчанию идут после полей без них в параметрах инициализатора. Оба списка (поля без значений по умолчанию и поля с ними) по-прежнему упорядочены в порядке определения.
Столкновение лицом к лицу с этой проблемой было одним из факторов, которые побудили меня написать замену для классов данных. Обходные пути, описанные здесь, хотя и полезны, требуют, чтобы код был искажен до такой степени, что они полностью сводят на нет наивный подход классов данных преимущества удобочитаемости (при котором упорядочение полей тривиально предсказуемо).
Когда вы используете наследование 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,
)
Затем вы можете объединить классы данных следующим образом. Обратите внимание, что мы можем объединить и и поля по умолчанию
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
на самом деле не наследуется от
A
а также
B
, что означает, что вы не можете использовать
isinstance()
или утверждения другого типа для проверки происхождения C.
Как насчет определения
ugly
поле, как это, вместо способа по умолчанию?
ugly: bool = field(metadata=dict(required=False, missing=False))
Экспериментальным, но интересным решением было бы использование метаклассов. Приведенное ниже решение позволяет использовать классы данных 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
Попытка сделать то же самое с помощью декоратора в каждом родительском классе вызвала бы исключение в следующем дочернем классе, например
TypeError(f'non-default argument <field-name) follows default argument')
. Приведенное выше решение предотвращает это, потому что поля сначала переупорядочиваются. Однако, поскольку порядок полей изменен, предотвращение
*args
использование в
DataClassCustom.__new__
является обязательным, так как первоначальный заказ больше не действителен.
Хотя в Python >= 3.10
kw_only
была введена функция, которая по существу делает наследование в классах данных намного более надежным, приведенный выше пример по-прежнему можно использовать как способ сделать наследуемые классы данных, которые не требуют использования
@dataclass
декоратор.
Дополняя решение Martijn Pieters, использующее attrs : можно создать наследование без репликации атрибутов по умолчанию с помощью:
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
параметр можно найти здесь