Die Art und Weise, wie Datenklassen Attribute kombinieren, verhindert, dass Sie Attribute mit Standardwerten in einer Basisklasse verwenden und dann Attribute ohne Standard (Positionsattribute) in einer Unterklasse verwenden können.
Dies liegt daran, dass die Attribute kombiniert werden, indem Sie am unteren Rand des MRO beginnen und eine geordnete Liste der Attribute in der zuerst gesehenen Reihenfolge erstellen. Überschreibungen werden an ihrem ursprünglichen Ort aufbewahrt. So Parent
beginnt mit ['name', 'age', 'ugly']
, wo ugly
eine Standard hat, und dann Child
fügt ['school']
an das Ende der Liste (mit ugly
bereits in der Liste). Dies bedeutet, dass Sie am Ende eine ungültige Argumentliste für haben, ['name', 'age', 'ugly', 'school']
da school
dies keine Standardeinstellung hat __init__
.
Dies ist in PEP-557- Datenklassen unter Vererbung dokumentiert :
Wenn die Datenklasse vom @dataclass
Dekorateur erstellt wird, durchsucht sie alle Basisklassen der Klasse in umgekehrter MRO (dh beginnend mit object
) und fügt für jede gefundene Datenklasse die Felder dieser Basisklasse einer geordneten hinzu Zuordnung von Feldern. Nachdem alle Basisklassenfelder hinzugefügt wurden, werden der geordneten Zuordnung eigene Felder hinzugefügt. Alle generierten Methoden verwenden diese kombinierte, berechnete geordnete Zuordnung von Feldern. Da die Felder in der Einfügereihenfolge sind, überschreiben abgeleitete Klassen Basisklassen.
und unter Spezifikation :
TypeError
wird ausgelöst, wenn ein Feld ohne Standardwert auf ein Feld mit einem Standardwert folgt. Dies gilt entweder, wenn dies in einer einzelnen Klasse auftritt oder als Ergebnis der Klassenvererbung.
Sie haben hier einige Optionen, um dieses Problem zu vermeiden.
Die erste Option besteht darin, separate Basisklassen zu verwenden, um Felder mit Standardwerten an eine spätere Position in der MRO-Reihenfolge zu zwingen. Vermeiden Sie es auf jeden Fall, Felder direkt für Klassen festzulegen, die als Basisklassen verwendet werden sollen, z Parent
.
Die folgende Klassenhierarchie funktioniert:
@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
Durch Herausziehen von Feldern in separate Basisklassen mit Feldern ohne Standardwerte und Felder mit Standardwerten und einer sorgfältig ausgewählten Vererbungsreihenfolge können Sie eine MRO erstellen, bei der alle Felder ohne Standardwerte vor denen mit Standardwerten platziert werden. Die umgekehrte MRO (ignoriert object
) für Child
ist:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
Beachten Sie, dass Parent
keine neuen Felder festgelegt werden. Daher spielt es hier keine Rolle, dass es in der Reihenfolge der Feldlisten als "letztes" endet. Die Klassen mit Feldern ohne Standardwerte ( _ParentBase
und _ChildBase
) stehen vor den Klassen mit Feldern mit Standardwerten ( _ParentDefaultsBase
und _ChildDefaultsBase
).
Das Ergebnis ist Parent
und Child
Klassen mit einem vernünftigen Feld älter, während Child
noch eine Unterklasse von 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
So können Sie Instanzen beider Klassen erstellen:
>>> 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)
Eine andere Möglichkeit besteht darin, nur Felder mit Standardwerten zu verwenden. Sie können immer noch den Fehler machen, keinen school
Wert anzugeben, indem Sie einen in erhöhen __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'")
aber dies tut die Feldreihenfolge ändern; school
endet nach ugly
:
<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>
und eine Art Hinweis checker wird bemängelt _no_default
kein String sein.
Sie können auch das attrs
Projekt verwenden , das Sie inspiriert hat dataclasses
. Es wird eine andere Strategie zum Zusammenführen von Vererbungen verwendet. es zieht überschriebene Felder in einer Unterklasse zum Ende der Liste Felder, so ['name', 'age', 'ugly']
in der Parent
Klasse wird ['name', 'age', 'school', 'ugly']
in der Child
Klasse; Durch Überschreiben des Felds mit einer Standardeinstellung wird attrs
das Überschreiben ermöglicht, ohne dass ein MRO-Tanz ausgeführt werden muss.
attrs
Unterstützt das Definieren von Feldern ohne Typhinweise, bleibt jedoch beim unterstützten Typhinweismodus, indem Sie Folgendes festlegen 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
attr.ib(kw_only=True)
, siehe github.com/python-attrs/attrs/issues/38Dieser Fehler wird angezeigt, weil nach einem Argument mit einem Standardwert ein Argument ohne Standardwert hinzugefügt wird. Die Einfügereihenfolge geerbter Felder in die Datenklasse ist die Umkehrung der Reihenfolge der Methodenauflösung. Dies bedeutet, dass die
Parent
Felder an erster Stelle stehen, auch wenn sie später von ihren untergeordneten Elementen überschrieben werden .Ein Beispiel aus PEP-557 - Datenklassen :
Leider glaube ich nicht, dass es einen Ausweg gibt. Mein Verständnis ist, dass, wenn die übergeordnete Klasse ein Standardargument hat, keine untergeordnete Klasse nicht standardmäßige Argumente haben kann.
quelle
Sie können Attribute mit Standardwerten in übergeordneten Klassen verwenden, wenn Sie sie von der Init-Funktion ausschließen. Wenn Sie die Möglichkeit benötigen, die Standardeinstellung bei init zu überschreiben, erweitern Sie den Code mit der Antwort von 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
quelle
Basierend auf der Martijn Pieters-Lösung habe ich Folgendes getan:
1) Erstellen Sie eine Mischung, die post_init implementiert
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) Dann in den Klassen mit dem Vererbungsproblem:
from src.utils import no_default, NoDefaultAttributesChild @dataclass class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin): attr1: str = no_default
BEARBEITEN:
Nach einiger Zeit finde ich auch Probleme mit dieser Lösung mit mypy. Der folgende Code behebt das 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
quelle
Der folgende Ansatz behandelt dieses Problem bei Verwendung von reinem Python
dataclasses
und ohne viel Boilerplate-Code.Das
ugly_init: dataclasses.InitVar[bool]
dient als Pseudofeld , um uns bei der Initialisierung zu helfen, und geht verloren, sobald die Instanz erstellt wurde. Whileugly: bool = field(init=False)
ist ein Instanzmitglied, das nicht mit der__init__
Methode initialisiert wird , sondern alternativ mit der__post_init__
Methode initialisiert werden kann (weitere Informationen finden Sie hier .).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()
quelle
Ich bin auf diese Frage zurückgekommen, nachdem ich festgestellt hatte, dass Datenklassen möglicherweise einen Decorator-Parameter erhalten, mit dem Felder neu angeordnet werden können. Dies ist sicherlich eine vielversprechende Entwicklung, obwohl die Entwicklung dieser Funktion etwas ins Stocken geraten zu sein scheint.
Im Moment können Sie dieses Verhalten und einige andere Besonderheiten durch die Verwendung von Datenklassen erreichen , meiner Neuimplementierung von Datenklassen, die solche Frustrationen überwinden. Die Verwendung
from dataclassy
anstelle vonfrom dataclasses
im ursprünglichen Beispiel bedeutet, dass es fehlerfrei ausgeführt wird.Durch die Verwendung von inspect zum Drucken der Unterschrift von wird
Child
deutlich, was gerade passiert. das Ergebnis ist(name: str, age: int, school: str, ugly: bool = True)
. Felder werden immer neu angeordnet, sodass Felder mit Standardwerten nach Feldern ohne diese in den Parametern zum Initialisierer kommen. Beide Listen (Felder ohne Standardeinstellungen und die mit ihnen) sind weiterhin in der Definitionsreihenfolge angeordnet.Die persönliche Auseinandersetzung mit diesem Problem war einer der Faktoren, die mich dazu veranlassten, einen Ersatz für Datenklassen zu schreiben. Die hier beschriebenen Problemumgehungen sind zwar hilfreich, erfordern jedoch, dass der Code so stark verzerrt wird, dass der naive Ansatz der Datenklassen, bei dem die Feldreihenfolge trivial vorhersehbar ist, vollständig zunichte gemacht wird.
quelle
Eine mögliche Problemumgehung besteht darin, die übergeordneten Felder mithilfe von Affen-Patches anzuhängen
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)
Es ist auch möglich, prepend Nicht-Standardfelder, indem geprüft wird
if f.default is dc.MISSING
, aber das ist wahrscheinlich zu schmutzig.Während beim Affen-Patching einige Vererbungsmerkmale fehlen, können dennoch Methoden zu allen Pseudo-Kind-Klassen hinzugefügt werden.
Legen Sie für eine genauere Steuerung die Standardwerte mit fest
dc.field(compare=False, repr=True, ...)
quelle
Sie können eine geänderte Version von Datenklassen verwenden, die nur eine Schlüsselwortmethode generiert
__init__
: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)
(auch als Kernstück veröffentlicht , getestet mit Python 3.6 Backport)
Dazu muss die untergeordnete Klasse als definiert werden
@dataclass_keyword_only class Child(Parent): school: str ugly: bool = True
Und würde generieren
__init__(self, *, name:str, age:int, ugly:bool=True, school:str)
(was gültige Python ist). Die einzige Einschränkung besteht darin, dass Objekte nicht mit Positionsargumenten initialisiert werden dürfen. Andernfalls handelt es sich um eine ganz normaledataclass
Version ohne hässliche Hacks.quelle