Спосіб поєднання атрибутів класів даних перешкоджає можливості використовувати атрибути за замовчуванням у базовому класі, а потім використовувати атрибути без типових (позиційні атрибути) у підкласі.
Це тому, що атрибути комбінуються, починаючи з нижньої частини 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