Ответ 1
То, как классы данных комбинируют атрибуты, не позволяет вам использовать атрибуты со значениями по умолчанию в базовом классе, а затем использовать атрибуты без значений по умолчанию (позиционные атрибуты) в подклассе.
Это потому, что атрибуты объединяются, начиная с нижней части MRO и создавая упорядоченный список атрибутов в порядке первого просмотра; переопределения хранятся в их исходном местоположении. Итак, Parent
начинается с ['name', 'age', 'ugly']
, где ugly
значение по умолчанию, а затем Child
добавляет ['school']
в конец этого списка (с уже ugly
в списке). Это означает, что вы в конечном итоге получите ['name', 'age', 'ugly', 'school']
и поскольку school
не имеет значения по умолчанию, это приводит к неверному списку аргументов для __init__
.
Это задокументировано в классах данных PEP-557 при наследовании:
Когда класс данных
@dataclass
декоратором@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)
Другой вариант - использовать только поля со значениями по умолчанию; вы все равно можете сделать ошибку, чтобы не __post_init__
значение 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
типов, устанавливая 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