То, как классы данных объединяют атрибуты, не позволяет вам использовать атрибуты со значениями по умолчанию в базовом классе, а затем использовать атрибуты без значений по умолчанию (позиционные атрибуты) в подклассе.
Это связано с тем, что атрибуты объединяются, начиная с нижней части 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