Klassenvererbung in Python 3.7-Datenklassen

84

Derzeit versuche ich mich an den neuen Datenklassenkonstruktionen, die in Python 3.7 eingeführt wurden. Ich bin gerade dabei zu versuchen, eine Vererbung einer Elternklasse vorzunehmen. Es sieht so aus, als würde die Reihenfolge der Argumente durch meinen aktuellen Ansatz so verpfuscht, dass der bool-Parameter in der untergeordneten Klasse vor den anderen Parametern übergeben wird. Dies verursacht einen 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 Folgendes TypeError:

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

Wie behebe ich das?

Mysterio
quelle

Antworten:

126

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 Parentbeginnt mit ['name', 'age', 'ugly'], wo uglyeine Standard hat, und dann Childfügt ['school']an das Ende der Liste (mit uglybereits in der Liste). Dies bedeutet, dass Sie am Ende eine ungültige Argumentliste für haben, ['name', 'age', 'ugly', 'school']da schooldies keine Standardeinstellung hat __init__.

Dies ist in PEP-557- Datenklassen unter Vererbung dokumentiert :

Wenn die Datenklasse vom @dataclassDekorateur 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 :

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

# 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 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 Childist:

_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent

Beachten Sie, dass Parentkeine 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 ( _ParentBaseund _ChildBase) stehen vor den Klassen mit Feldern mit Standardwerten ( _ParentDefaultsBaseund _ChildDefaultsBase).

Das Ergebnis ist Parentund ChildKlassen mit einem vernünftigen Feld älter, während Childnoch 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 schoolWert 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; schoolendet 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_defaultkein String sein.

Sie können auch das attrsProjekt 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 ParentKlasse wird ['name', 'age', 'school', 'ugly']in der ChildKlasse; Durch Überschreiben des Felds mit einer Standardeinstellung wird attrsdas Überschreiben ermöglicht, ohne dass ein MRO-Tanz ausgeführt werden muss.

attrsUnterstü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
Martijn Pieters
quelle
1
Vielen Dank für die ausführliche Antwort
Mysterio
Das ist sehr hilfreich. Ich bin allerdings verwirrt über den mro. Beim Ausführen von print (Child.mro ()) wird Folgendes angezeigt: [<class ' main .Child'>, <class ' main .Parent'>, <class ' main ._ChildDefaultsBase'>, <class ' main ._ParentDefaultsBase'>, < Klasse ' main ._ChildBase'>, <Klasse ' main ._ParentBase'>, <Klasse 'Objekt'>] Gehen die Standardbasen also nicht den Basisklassen voraus?
Ollie
1
@Ollie das ist die richtige Reihenfolge; Beachten Sie, dass ich es in meiner Antwort aufgeführt habe. Wenn Sie mehrere Basisklassen haben, müssen Sie die beteiligten Klassen linearisieren , um zu entscheiden, welche Klassen beim Erben vor anderen stehen. Python verwendet die C3-Linearisierungsmethode und meine Antwort nutzt die Funktionsweise, um sicherzustellen, dass Attribute mit Standardwerten immer nach allen Attributen ohne Standardwerte stehen.
Martijn Pieters
Eigentlich können attrs funktionieren, aber Sie müssen sie verwenden attr.ib(kw_only=True), siehe github.com/python-attrs/attrs/issues/38
laike9m
8

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 der Reihenfolge der Methodenauflösung. Dies bedeutet, dass die ParentFelder an erster Stelle stehen, auch wenn sie später von ihren untergeordneten Elementen ü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 richtigen Reihenfolge x, y, z. Der endgültige Typ von xist int, wie in der Klasse angegeben C.

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.

Patrick Haugh
quelle
Ich verstehe, dass das nicht standardmäßige Argument vor dem standardmäßigen Argument stehen muss, aber wie kann es sein, wenn die übergeordneten Argumente vor dem Hinzufügen der untergeordneten Argumente initialisiert werden?
Mysterio
3
Ich glaube, daran führt leider kein Weg vorbei. Mein Verständnis ist, dass, wenn die übergeordnete Klasse ein Standardargument hat, keine untergeordnete Klasse nicht standardmäßige Argumente haben kann.
Patrick Haugh
1
Können Sie diese Informationen zur Antwort hinzufügen, bevor ich sie markiere? Es wird eines Tages jemandem helfen. Es ist ziemlich bedauerlich, dass Datenklassen eingeschränkt werden. Macht es zu meinem aktuellen Python-Projekt. Es ist schön, solche Implementierungen zu sehen, obwohl
Mysterio
5

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
SimonMarcin
quelle
Ich denke, diese Antwort sollte mehr anerkannt werden. Das Problem, ein Standardfeld in der übergeordneten Klasse zu haben, wurde behoben und der TypeError entfernt.
Nils Bengtsson
5

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
Daniel Albarral
quelle
Wollten Sie oben in 2) "Klasse MyDataclass (DataclassWithDefaults, NoDefaultAttributesPostInitMixin)" schreiben?
Scott P.
4

Der folgende Ansatz behandelt dieses Problem bei Verwendung von reinem Python dataclassesund 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. While ugly: 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()
Praveen Kulkarni
quelle
hässlich_init ist jetzt ein erforderlicher Parameter ohne Standard
Vadym Tyemirov
2

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 dataclassyanstelle von from dataclassesim ursprünglichen Beispiel bedeutet, dass es fehlerfrei ausgeführt wird.

Durch die Verwendung von inspect zum Drucken der Unterschrift von wird Childdeutlich, 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.

biqqles
quelle
1

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, ...)

Patrick
quelle
1

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 normale dataclassVersion ohne hässliche Hacks.

Noamk
quelle