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