Sposób, w jaki klasy danych łączy atrybuty, uniemożliwia użycie atrybutów z wartościami domyślnymi w klasie bazowej, a następnie użycie atrybutów bez wartości domyślnych (atrybuty pozycyjne) w podklasie.
Dzieje się tak, ponieważ atrybuty są łączone, zaczynając od dołu MRO i tworząc uporządkowaną listę atrybutów w kolejności od pierwszego zobaczenia; nadpisania są zachowywane w ich pierwotnej lokalizacji. Parent
Zaczyna się więc od ['name', 'age', 'ugly']
, gdzie ugly
ma wartość domyślną, a następnie Child
dodaje ['school']
na końcu tej listy (z ugly
już na liście). Oznacza to, że kończysz z, ['name', 'age', 'ugly', 'school']
a ponieważ school
nie ma wartości domyślnej, skutkuje to nieprawidłowym wykazem argumentów dla __init__
.
Jest to udokumentowane w klasach danych PEP-557 , w ramach dziedziczenia :
Gdy klasa danych jest tworzona przez @dataclass
dekorator, przegląda wszystkie klasy bazowe klasy w odwrotnym MRO (czyli zaczynając od object
) i dla każdej znalezionej klasy danych dodaje pola z tej klasy bazowej do uporządkowanej mapowanie pól. Po dodaniu wszystkich pól klasy bazowej dodaje własne pola do uporządkowanego mapowania. Wszystkie wygenerowane metody wykorzystają to połączone, obliczone uporządkowane mapowanie pól. Ponieważ pola są w kolejności wstawiania, klasy pochodne zastępują klasy podstawowe.
i pod specyfikacją :
TypeError
zostanie podniesiony, jeśli pole bez wartości domyślnej następuje po polu z wartością domyślną. Dzieje się tak, gdy dzieje się to w jednej klasie lub w wyniku dziedziczenia klas.
Masz tutaj kilka opcji, aby uniknąć tego problemu.
Pierwszą opcją jest użycie oddzielnych klas bazowych, aby wymusić umieszczenie pól z wartościami domyślnymi na późniejszej pozycji w kolejności MRO. Za wszelką cenę unikaj ustawiania pól bezpośrednio w klasach, które mają być używane jako klasy bazowe, takich jak Parent
.
Działa następująca hierarchia klas:
@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
Wyciągając pola do oddzielnych klas bazowych z polami bez wartości domyślnych i polami z wartościami domyślnymi oraz starannie dobraną kolejnością dziedziczenia, można utworzyć MRO, które umieszcza wszystkie pola bez wartości domyślnych przed polami z wartościami domyślnymi. Odwrócona MRO (ignorująca object
) dla Child
to:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
Zwróć uwagę, że Parent
nie ustawia to żadnych nowych pól, więc nie ma znaczenia, że kończy się jako ostatnie w kolejności wyświetlania pól. Klasy z polami bez wartości domyślnych ( _ParentBase
i _ChildBase
) poprzedzają klasy z polami z wartościami domyślnymi ( _ParentDefaultsBase
i _ChildDefaultsBase
).
Rezultatem jest Parent
i Child
klasy z rozsądnym polem starsze, podczas gdy Child
nadal jest podklasą 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
więc możesz tworzyć instancje obu klas:
>>> 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)
Inną opcją jest używanie tylko pól z wartościami domyślnymi; nadal możesz popełnić błąd, aby nie podać school
wartości, podnosząc ją w __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'")
ale nie zmieniają kolejność pól; school
kończy się po ugly
:
<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>
a narzędzie do sprawdzania podpowiedzi typu będzie narzekać, że _no_default
nie jest ciągiem znaków.
Możesz także skorzystać z attrs
projektu , który był projektem, który zainspirował dataclasses
. Używa innej strategii łączenia dziedziczenia; ciągnie pola przesłonięte w podklasie na końcu listy pól, więc ['name', 'age', 'ugly']
w Parent
klasie staje się ['name', 'age', 'school', 'ugly']
w Child
klasie; nadpisując pole wartością domyślną, attrs
pozwala na nadpisanie bez konieczności wykonywania tańca MRO.
attrs
obsługuje definiowanie pól bez podpowiedzi typu, ale pozwala trzymać się obsługiwanego trybu podpowiedzi typu poprzez ustawienie 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
ugly: bool = True
= rekt :)