Python-Kurs / Datenklassen mit dataclass und unabhängigen Standardwerten erstellen

dataclass erzeugt aus Felddeklarationen Methoden für Initialisierung, Darstellung und Gleichheitsvergleich. Das vereinfacht datenorientierte Klassen, erzwingt aber weder die angegebenen Typen noch fachliche Regeln automatisch.

Wiederkehrende Methoden aus Feldern erzeugen lassen

Der Dekorator @dataclass findet die Felder anhand der Typannotationen im Klassenkörper. Mit den üblichen Standardeinstellungen erzeugt er unter anderem __init__, __repr__ und __eq__, soweit die entsprechenden eigenen Implementierungen und Einstellungen dies zulassen. Sie müssen diese wiederkehrenden Teile für einen einfachen Datensatz nicht von Hand ausschreiben.

Eine Annotation wie amount: int beschreibt den beabsichtigten Typ, prüft ihn aber nicht bei jeder Ausführung. Ebenso garantiert tags: list[str] nicht automatisch, dass ein übergebener Wert eine Liste ist oder jedes Element eine Zeichenkette. Die zusätzliche Betragsprüfung dieser Übung wird ausdrücklich im Code geschrieben. Die Dokumentation zu dataclasses beschreibt generierte Methoden, Felder und Einstellungen.

Für ein eigenes Standardlistenobjekt pro Instanz wird field(default_factory=list) verwendet. Übergeben wird die aufrufbare Funktion list, nicht das Ergebnis list(). Die Factory erstellt das Standardobjekt dann, wenn für das betreffende neue Objekt kein eigener Wert geliefert wurde. Diese Lösung knüpft an die Unterscheidung zwischen Klassen- und Instanzattributen an.

Unabhängige Tags und einen Feldvergleich ausführen

Speichern Sie den folgenden vollständigen Code als data_records.py in UTF-8 im Übungsordner. Starten Sie python data_records.py mit dem Projektinterpreter. Außerhalb einer aktivierten Umgebung kann unter macOS oder Ubuntu python3 passen. Das Modul gehört zur Standardbibliothek; Zusatzpakete und externe Dateien sind nicht nötig.

from dataclasses import dataclass, field

@dataclass
class Expense:
    name: str
    amount: int
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if type(self.amount) is not int or self.amount < 0:
            raise ValueError("invalid amount")

a = Expense("lunch", 8000)
b = Expense("bus", 2500)
a.tags.append("work")
print(a)
print(b.tags)
print(a == Expense("lunch", 8000, ["work"]))

Erwartete Ausgabe:

Expense(name='lunch', amount=8000, tags=['work'])
[]
True

Die erzeugte Initialisierung erhält Name und Betrag und erzeugt bei ausgelassenen Tags eine neue Liste. a.tags.append("work") ändert deshalb nicht b.tags. Die erste Ausgabe verwendet die erzeugte Darstellung mit Klassenname, Feldnamen und Werten; sie ist eine nützliche Diagnoseform, aber kein eigens festgelegtes Austauschformat.

Der letzte Ausdruck vergleicht zwei getrennte Expense-Objekte mit denselben Feldwerten. Der Vergleich ist wahr, obwohl die Objekte nicht identisch sind. Bei diesen normalen Feldern wird auch die Tagliste in den Vergleich einbezogen. Ein anderer Name, ein anderer Betrag oder andere Tags können daher das Ergebnis ändern.

Initiale Prüfung und spätere Veränderung unterscheiden

Die erzeugte __init__-Methode ruft anschließend __post_init__ auf. Dort weist dieses Beispiel negative Beträge und Werte zurück, deren genauer Typ nicht der eingebaute int ist. Damit werden unter anderem True, 3.5 und eine Zahlenzeichenkette als Anfangsbetrag abgelehnt.

Die Prüfung erfolgt nicht automatisch erneut bei a.amount = -1. Die unveränderte Klasse lässt eine solche spätere direkte Änderung zu. Wenn die Regel über die gesamte Lebensdauer gewöhnlicher Zuweisungen erhalten bleiben muss, benötigen Sie eine andere passende Konstruktion, beispielsweise eine Property mit Setterprüfung oder einen bewusst unveränderlichen Datensatz.

Mit @dataclass(frozen=True) können gewöhnliche Feldzuweisungen eingeschränkt werden. Ein Versuch, a.amount neu zuzuweisen, löst dann FrozenInstanceError aus. Die im Feld referenzierte Liste wird dadurch jedoch nicht ebenfalls eingefroren: a.tags.append("extra") kann weiterhin den Inhalt ändern. Für eine tiefer unveränderliche Struktur müssen auch die enthaltenen Daten entsprechend gestaltet werden, etwa mit passenden Tupeln und unveränderlichen Elementen.

Wird eine vorhandene Tagliste ausdrücklich an den Initialisierer übergeben, kopiert default_factory diese nicht automatisch. Die Factory ist für den fehlenden Standardwert zuständig. Bei bewusst übergebenen Objekten muss der Vertrag weiterhin festlegen, ob eine gemeinsame Referenz oder eine unabhängige Kopie gewünscht ist.

Vergleich, fachliche Identität und Umwandlung prüfen

  1. Prüfen Sie a.tags is not b.tags und ergänzen Sie getrennt einen Tag an b. Die Liste von a darf sich dadurch nicht ändern.
  2. Erzeugen Sie getrennt einen Datensatz mit Betrag -1 und danach einen mit True. Beide müssen wegen der ausdrücklich geschriebenen Prüfung abgelehnt werden. Stellen Sie die gültigen Eingaben wieder her.
  3. Vergleichen Sie ein zweites Objekt mit identischen Feldwerten mit a. == kann wahr sein, während is falsch ist. Ändern Sie nur dessen Tags und prüfen Sie den Vergleich erneut.
  4. Testen Sie eine gesonderte Variante mit frozen=True: Feldzuweisung wird zurückgewiesen, das Anhängen an die vorhandene Tagliste bleibt möglich. Stellen Sie danach den ursprünglichen Dekorator und die Testwerte wieder her.

Gleiche Feldwerte beweisen keine fachliche Transaktionsidentität. Zwei tatsächliche Ausgaben können denselben Namen, Betrag und dieselben Tags besitzen und trotzdem verschiedene Vorgänge sein. Planen Sie bei Bedarf eine eigene Kennung und eine ausdrücklich passende Regel für Duplikate.

dataclasses.asdict(a) erstellt eine Wörterbuchdarstellung und verarbeitet dabei verschachtelte Daten entsprechend seiner Konvertierungsregeln. Das ist noch keine allgemeine JSON-Serialisierung: Ein enthaltenes Datum oder Decimal-Objekt wird dadurch nicht automatisch zu einem geeigneten JSON-Wert. Die Übung zu JSON und Datenvalidierung erklärt diese Grenze. Testen Sie Objektaufbau, Datenprüfung, Gleichheit und Speicherung daher als unterschiedliche Aufgaben.