it-swarm.dev

Klassenvererbung in Python 3.7-Datenklassen

Momentan versuche ich mich an den neuen Datenklassenkonstruktionen, die in Python 3.7 eingeführt wurden. Momentan versuche ich nicht, eine Vererbung einer übergeordneten Klasse durchzuführen. Die Reihenfolge der Argumente scheint zu stimmen Mein aktueller Ansatz hat mich so verpfuscht, dass der bool-Parameter in der untergeordneten Klasse vor den anderen Parametern übergeben wird. Dies führt zu einem Typfehler.

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()

Wenn ich diesen Code ausführe, erhalte ich diesen TypeError:

TypeError: non-default argument 'school' follows default argument

Wie behebe ich das?

30
Mysterio

Die Art und Weise, wie Datenklassen Attribute kombinieren, verhindert, dass Sie Attribute mit Standardwerten in einer Basisklasse und anschließend Attribute ohne Standardwert (Positionsattribute) in einer Unterklasse verwenden können.

Dies liegt daran, dass die Attribute kombiniert werden, indem Sie am unteren Rand der MRO beginnen und eine geordnete Liste der Attribute in der Reihenfolge erstellen, in der sie zuerst angezeigt werden. Überschreibungen werden an ihrem ursprünglichen Speicherort aufbewahrt. Also beginnt Parent mit ['name', 'age', 'ugly'], Wobei ugly einen Standardwert hat, und Child fügt ['school'] Am Ende dieser Liste hinzu ( mit ugly bereits in der Liste). Dies bedeutet, dass Sie am Ende ['name', 'age', 'ugly', 'school'] Haben und da school keinen Standardwert hat, führt dies zu einer ungültigen Argumentliste für __init__.

Dies ist in PEP-557 Dataclasses unter inheritance :

Wenn die Datenklasse vom Dekorator @dataclass Erstellt wird, werden alle Basisklassen der Klasse in umgekehrter MRO (dh beginnend mit object) und für jede Datenklasse, die sie enthält, durchsucht findet, fügt die Felder dieser Basisklasse zu einer geordneten Zuordnung von Feldern hinzu. Nachdem alle Basisklassenfelder hinzugefügt wurden, werden dem geordneten Mapping eigene Felder hinzugefügt. Alle generierten Methoden verwenden diese kombinierte, berechnete geordnete Zuordnung von Feldern. Da die Felder in der Reihenfolge der Einfügung sind, überschreiben abgeleitete Klassen Basisklassen.

und unter Spezifikation :

TypeError wird ausgelöst, wenn ein Feld ohne Standardwert auf ein Feld mit Standardwert folgt. Dies ist entweder der Fall, wenn dies in einer einzelnen Klasse auftritt, oder als Ergebnis der Klassenvererbung.

Sie haben hier einige Möglichkeiten, um dieses Problem zu vermeiden.

Die erste Möglichkeit besteht darin, separate Basisklassen zu verwenden, um Felder mit Standardwerten an eine spätere Position in der MRO-Reihenfolge zu zwingen. Vermeiden Sie auf jeden Fall, Felder direkt in Klassen festzulegen, die als Basisklassen verwendet werden sollen, z. B. Parent.

Die folgende Klassenhierarchie funktioniert:

# 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

Durch Herausziehen von Feldern in separate Basisklassen mit Feldern ohne Standardwerte und Feldern mit Standardwerten und einer sorgfältig ausgewählten Vererbungsreihenfolge können Sie einen MRO erstellen, der alle setzt Felder ohne Standardwerte vor denen mit Standardwerten. Die umgekehrte MRO (ignoriert object) für Child ist:

_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent

Beachten Sie, dass Parent keine neuen Felder festlegt. Daher spielt es keine Rolle, dass es in der Reihenfolge der Feldauflistungen als letztes angezeigt wird. Die Klassen mit Feldern ohne Standard (_ParentBase Und _ChildBase) Stehen vor den Klassen mit Feldern mit Standard (_ParentDefaultsBase Und _ChildDefaultsBase).

Das Ergebnis sind Klassen Parent und Child mit einem vernünftigen Feld älter, während Child immer noch eine Unterklasse von Parent ist:

>>> 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

und 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 Option besteht darin, nur Felder mit Standardeinstellungen zu verwenden. Sie können immer noch einen Fehler machen, wenn Sie keinen school -Wert angeben, indem Sie einen in __post_init__ erhöhen:

_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 verändert die Halbbildreihenfolge; school endet nach ugly:

<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>

und ein Typ-Hinweis-Checker wird darüber klagen, dass _no_default kein String ist.

Sie können auch das attrs Projekt verwenden, das das Projekt war, das dataclasses inspiriert hat. Es wird eine andere Strategie zum Zusammenführen von Vererbungen verwendet. überschriebene Felder in einer Unterklasse werden an das Ende der Feldliste gezogen, sodass ['name', 'age', 'ugly'] in der Klasse Parent zu ['name', 'age', 'school', 'ugly'] in der Klasse Child wird; Durch Überschreiben des Felds mit einem Standardwert ermöglicht attrs das Überschreiben, ohne dass ein MRO-Tanz ausgeführt werden muss.

attrs unterstützt das Definieren von Feldern ohne Typhinweise, lässt sich aber durch Setzen von auto_attribs=True auf nterstützter Typhinweismodus beschränken:

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
35
Martijn Pieters

Dieser 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 von Methodenauflösungsreihenfolge , was bedeutet, dass die Parent -Felder an erster Stelle stehen, auch wenn sie später von ihren untergeordneten Feldern überschrieben werden.

Ein Beispiel aus PEP-557 - Datenklassen :

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

Die endgültige Liste der Felder ist in der Reihenfolge x, y, z. Der letzte Typ von x ist int, wie in Klasse C angegeben.

Leider glaube ich nicht, dass es einen Ausweg gibt. Ich verstehe, dass keine untergeordnete Klasse Nicht-Standardargumente haben kann, wenn die übergeordnete Klasse ein Standardargument hat.

7
Patrick Haugh

basierend auf Martijn Pieters Lösung habe ich folgendes gemacht:

1) Erstellen Sie ein Mixing, das 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
4
Daniel Albarral

Der folgende Ansatz beschäftigt sich mit diesem Problem bei Verwendung von purem python dataclasses und ohne viel Boilerplate-Code.

Der ugly_init: dataclasses.InitVar[bool] Dient als Pseudofeld , um uns bei der Initialisierung zu helfen. Er geht verloren, sobald die Instanz erstellt wurde. Während ugly: bool = field(init=False) ein Instanzmitglied ist, das nicht mit der __init__ - Methode initialisiert wird, sondern alternativ mit der __post_init__ - Methode initialisiert werden kann (weitere 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()
3