Obecnie próbuję swoich sił w nowych konstrukcjach klas danych wprowadzonych w Pythonie 3.7. Obecnie utknąłem na próbach dziedziczenia klasy nadrzędnej. Wygląda na to, że kolejność argumentów została zepsuta przez moje obecne podejście, tak że parametr bool w klasie potomnej jest przekazywany przed innymi parametrami. Powoduje to błąd typu.
from dataclasses import dataclass
@dataclass
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')
@dataclass
class Child(Parent):
school: str
ugly: bool = True
jack = Parent('jack snr', 32, ugly=True)
jack_son = Child('jack jnr', 12, school = 'havard', ugly=True)
jack.print_id()
jack_son.print_id()
Po uruchomieniu tego kodu otrzymuję to TypeError
:
TypeError: non-default argument 'school' follows default argument
Jak to naprawić?
python
python-3.x
python-3.7
python-dataclasses
Mysterio
źródło
źródło
ugly: bool = True
= rekt :)Odpowiedzi:
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']
, gdzieugly
ma wartość domyślną, a następnieChild
dodaje['school']
na końcu tej listy (zugly
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 :
i pod specyfikacją :
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:
# base classes with fields; fields without defaults separate from fields with. @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 # public classes, deriving from base-with, base-without field classes # subclasses of public classes should put the public base class up front. @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
) dlaChild
to: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
iChild
klasy z rozsądnym polem starsze, podczas gdyChild
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ę pougly
:<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']
wParent
klasie staje się['name', 'age', 'school', 'ugly']
wChild
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 ustawienieauto_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
źródło
attr.ib(kw_only=True)
, zobacz github.com/python-attrs/attrs/issues/38Widzisz ten błąd, ponieważ argument bez wartości domyślnej jest dodawany po argumencie z wartością domyślną. Kolejność wstawiania dziedziczonych pól do klasy danych jest odwrotnością kolejności rozwiązywania metod , co oznacza, że
Parent
pola zajmują pierwsze miejsce, nawet jeśli zostaną później nadpisane przez ich dzieci.Przykład z PEP-557 - Klasy danych :
Niestety nie sądzę, aby można było to obejść. Rozumiem, że jeśli klasa nadrzędna ma argument domyślny, żadna klasa podrzędna nie może mieć argumentów innych niż domyślne.
źródło
Możesz używać atrybutów z wartościami domyślnymi w klasach nadrzędnych, jeśli wykluczysz je z funkcji init. Jeśli potrzebujesz możliwości nadpisania wartości domyślnej w init, rozszerz kod o odpowiedź Praveen Kulkarni.
from dataclasses import dataclass, field @dataclass class Parent: name: str age: int ugly: bool = field(default=False, init=False) @dataclass class Child(Parent): school: str jack = Parent('jack snr', 32) jack_son = Child('jack jnr', 12, school = 'havard') jack_son.ugly = True
źródło
w oparciu o rozwiązanie Martijn Pieters wykonałem następujące czynności:
1) Utwórz miksowanie implementujące post_init
from dataclasses import dataclass no_default = object() @dataclass class NoDefaultAttributesPostInitMixin: def __post_init__(self): for key, value in self.__dict__.items(): if value is no_default: raise TypeError( f"__init__ missing 1 required argument: '{key}'" )
2) Następnie na zajęciach z problemem dziedziczenia:
from src.utils import no_default, NoDefaultAttributesChild @dataclass class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin): attr1: str = no_default
EDYTOWAĆ:
Po jakimś czasie znajduję również problemy z tym rozwiązaniem z myPy, poniższy kod rozwiązuje problem.
from dataclasses import dataclass from typing import TypeVar, Generic, Union T = TypeVar("T") class NoDefault(Generic[T]): ... NoDefaultVar = Union[NoDefault[T], T] no_default: NoDefault = NoDefault() @dataclass class NoDefaultAttributesPostInitMixin: def __post_init__(self): for key, value in self.__dict__.items(): if value is NoDefault: raise TypeError(f"__init__ missing 1 required argument: '{key}'") @dataclass class Parent(NoDefaultAttributesPostInitMixin): a: str = "" @dataclass class Child(Foo): b: NoDefaultVar[str] = no_default
źródło
Poniższe podejście rozwiązuje ten problem podczas korzystania z czystego języka Python
dataclasses
i bez dużej ilości kodu standardowego.Do
ugly_init: dataclasses.InitVar[bool]
służy jako pseudo-pole tylko pomóc nam zrobić inicjalizacji i zostaną utracone po utworzeniu instancji. Whileugly: bool = field(init=False)
to element członkowski instancji, który nie zostanie zainicjowany__init__
metodą, ale może być alternatywnie zainicjowany przy użyciu__post_init__
metody (więcej można znaleźć tutaj ).from dataclasses import dataclass, field @dataclass class Parent: name: str age: int ugly: bool = field(init=False) ugly_init: dataclasses.InitVar[bool] def __post_init__(self, ugly_init: bool): self.ugly = ugly_init 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): school: str jack = Parent('jack snr', 32, ugly_init=True) jack_son = Child('jack jnr', 12, school='havard', ugly_init=True) jack.print_id() jack_son.print_id()
Jeśli chcesz użyć wzorca, w którym
ugly_init
jest opcjonalny, możesz zdefiniować metodę klasy w obiekcie nadrzędnym, która zawieraugly_init
jako opcjonalny parametr:from dataclasses import dataclass, field, InitVar @dataclass class Parent: name: str age: int ugly: bool = field(init=False) ugly_init: InitVar[bool] def __post_init__(self, ugly_init: bool): self.ugly = ugly_init @classmethod def create(cls, ugly_init=True, **kwargs): return cls(ugly_init=ugly_init, **kwargs) 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): school: str jack = Parent.create(name='jack snr', age=32, ugly_init=False) jack_son = Child.create(name='jack jnr', age=12, school='harvard') jack.print_id() jack_son.print_id()
Teraz możesz użyć
create
metody class jako metody fabrycznej do tworzenia klas nadrzędnych / podrzędnych z domyślną wartością dlaugly_init
. Zauważ, że musisz użyć nazwanych parametrów, aby to podejście działało.źródło
Wróciłem do tego pytania po odkryciu, że klasy danych mogą uzyskiwać parametr dekoratora, który umożliwia zmianę kolejności pól. Jest to z pewnością obiecująca zmiana, chociaż wydaje się, że rozwój tej funkcji nieco się zatrzymał.
W tej chwili możesz uzyskać to zachowanie i kilka innych subtelności, używając klasy danych , mojej ponownej implementacji klas danych, która przezwycięża takie frustracje. Użycie
from dataclassy
zamiastfrom dataclasses
w oryginalnym przykładzie oznacza, że działa on bez błędów.Używanie funkcji inspect do drukowania podpisu
Child
sprawia, że wszystko jest jasne; wynik jest(name: str, age: int, school: str, ugly: bool = True)
. Kolejność pól jest zawsze zmieniana, tak aby pola z wartościami domyślnymi występowały po polach bez nich w parametrach inicjatora. Obie listy (pola bez wartości domyślnych i te z nimi) są nadal uporządkowane w kolejności definicji.Stanie twarzą w twarz z tym problemem było jednym z czynników, które skłoniły mnie do napisania zamiennika klas danych. Opisane tutaj obejścia, choć pomocne, wymagają, aby kod był zniekształcony do takiego stopnia, że całkowicie neguje naiwne podejście klas danych, które zapewnia przewagę czytelności (gdzie porządkowanie pól jest trywialnie przewidywalne).
źródło
Możliwym obejściem jest użycie małpiego poprawiania w celu dołączenia pól nadrzędnych
import dataclasses as dc def add_args(parent): def decorator(orig): "Append parent's fields AFTER orig's fields" # Aggregate fields ff = [(f.name, f.type, f) for f in dc.fields(dc.dataclass(orig))] ff += [(f.name, f.type, f) for f in dc.fields(dc.dataclass(parent))] new = dc.make_dataclass(orig.__name__, ff) new.__doc__ = orig.__doc__ return new return decorator class Animal: age: int = 0 @add_args(Animal) class Dog: name: str noise: str = "Woof!" @add_args(Animal) class Bird: name: str can_fly: bool = True Dog("Dusty", 2) # --> Dog(name='Dusty', noise=2, age=0) b = Bird("Donald", False, 40) # --> Bird(name='Donald', can_fly=False, age=40)
Możliwe jest również dodawanie pól innych niż domyślne, przez zaznaczenie
if f.default is dc.MISSING
, ale prawdopodobnie jest to zbyt brudne.Chociaż małpie łatanie nie ma niektórych cech dziedziczenia, nadal można je stosować do dodawania metod do wszystkich klas pseudo-potomnych.
Aby uzyskać bardziej szczegółową kontrolę, ustaw wartości domyślne za pomocą
dc.field(compare=False, repr=True, ...)
źródło
Możesz użyć zmodyfikowanej wersji klas danych, która wygeneruje
__init__
metodę opartą tylko na słowach kluczowych :import dataclasses def _init_fn(fields, frozen, has_post_init, self_name): # fields contains both real fields and InitVar pseudo-fields. globals = {'MISSING': dataclasses.MISSING, '_HAS_DEFAULT_FACTORY': dataclasses._HAS_DEFAULT_FACTORY} body_lines = [] for f in fields: line = dataclasses._field_init(f, frozen, globals, self_name) # line is None means that this field doesn't require # initialization (it's a pseudo-field). Just skip it. if line: body_lines.append(line) # Does this class have a post-init function? if has_post_init: params_str = ','.join(f.name for f in fields if f._field_type is dataclasses._FIELD_INITVAR) body_lines.append(f'{self_name}.{dataclasses._POST_INIT_NAME}({params_str})') # If no body lines, use 'pass'. if not body_lines: body_lines = ['pass'] locals = {f'_type_{f.name}': f.type for f in fields} return dataclasses._create_fn('__init__', [self_name, '*'] + [dataclasses._init_param(f) for f in fields if f.init], body_lines, locals=locals, globals=globals, return_type=None) def add_init(cls, frozen): fields = getattr(cls, dataclasses._FIELDS) # Does this class have a post-init function? has_post_init = hasattr(cls, dataclasses._POST_INIT_NAME) # Include InitVars and regular fields (so, not ClassVars). flds = [f for f in fields.values() if f._field_type in (dataclasses._FIELD, dataclasses._FIELD_INITVAR)] dataclasses._set_new_attribute(cls, '__init__', _init_fn(flds, frozen, has_post_init, # The name to use for the "self" # param in __init__. Use "self" # if possible. '__dataclass_self__' if 'self' in fields else 'self', )) return cls # a dataclass with a constructor that only takes keyword arguments def dataclass_keyword_only(_cls=None, *, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False): def wrap(cls): cls = dataclasses.dataclass( cls, init=False, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen) return add_init(cls, frozen) # See if we're being called as @dataclass or @dataclass(). if _cls is None: # We're called with parens. return wrap # We're called as @dataclass without parens. return wrap(_cls)
(również opublikowane jako sedno , przetestowane z backportem Python 3.6)
Będzie to wymagało zdefiniowania klasy podrzędnej jako
@dataclass_keyword_only class Child(Parent): school: str ugly: bool = True
I wygeneruje
__init__(self, *, name:str, age:int, ugly:bool=True, school:str)
(co jest prawidłowym Pythonem). Jedynym zastrzeżeniem jest to, że nie pozwala na inicjalizację obiektów za pomocą argumentów pozycyjnych, ale poza tym jest to całkowicie normalnedataclass
bez brzydkich hacków.źródło