Спосіб поєднання атрибутів класів даних перешкоджає можливості використовувати атрибути за замовчуванням у базовому класі, а потім використовувати атрибути без типових (позиційні атрибути) у підкласі.
Це тому, що атрибути комбінуються, починаючи з нижньої частини MRO, і складаючи впорядкований список атрибутів у першочерговому порядку; заміни зберігаються у вихідному місці. Тож Parentпочинається з ['name', 'age', 'ugly'], де uglyмає значення за замовчуванням, а потім Childдодається ['school']до кінця цього списку (з uglyуже в списку). Це означає, що у вас вийшло, ['name', 'age', 'ugly', 'school']і оскільки у schoolнього немає типового значення, це призводить до недійсного переліку аргументів для __init__.
Це задокументовано в класах даних PEP-557, що передаються у спадок :
Коли @dataclassдекоратор створює клас даних , він переглядає всі базові класи класу в зворотному MRO (тобто починаючи з object) і для кожного знайденого класу даних додає поля з цього базового класу до впорядкованого картографування полів. Після додавання всіх полів базового класу він додає власні поля до впорядкованого відображення. Усі створені методи використовуватимуть це комбіноване, розраховане впорядковане відображення полів. Оскільки поля в порядку вставки, похідні класи замінюють базові класи.
і згідно специфікації :
TypeErrorбуде піднято, якщо поле без значення за замовчуванням слідує за полем зі значенням за замовчуванням. Це справедливо або тоді, коли це відбувається в одному класі, або в результаті успадкування класу.
У вас є кілька варіантів, щоб уникнути цієї проблеми.
Перший варіант полягає у використанні окремих базових класів, щоб примусити поля зі значеннями за замовчуванням пізніше в порядку MRO. За будь-яку ціну уникайте встановлення полів безпосередньо для класів, які слід використовувати як базові класи, наприклад 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