dataclasses: Datenklassen in Python
1. Übersicht
Wenn Sie eine Klasse nur zum Speichern von Daten schreiben (wie Order, Customer, Point…), müssen Sie normalerweise selbst __init__, __repr__, __eq__ schreiben — alles ist wiederholter Code. Das dataclasses-Modul (seit Python 3.7 verfügbar) generiert diese Methoden automatisch mit nur einem Decorator. Nach diesem Artikel wissen Sie, wie Sie @dataclass verwenden, um Datenklassen prägnant zu schreiben, Daten mit __post_init__ zu validieren, Klassen unveränderlich zu machen und in dict/JSON umzuwandeln.
2. Was ist eine grundlegende Dataclass?
@dataclass ist ein Decorator, der Type-Hints in der Klasse liest und dann automatisch __init__, __repr__, __eq__ auf Basis dieser Felder generiert.
from dataclasses import dataclass
@dataclass
class Customer:
name: str
email: str
age: int = 0
@dataclassscannt Attribute mit Type-Hints (name,email,age) und behandelt diese als Felder.age: int = 0hat einen Standardwert, ähnlich wie ein Standardparameter in einer Funktion.- Sie müssen nicht
__init__schreiben — es wird automatisch gemäß der Feldersortierung generiert.
c1 = Customer("An", "an@example.com")
print(c1)
Customer(name='An', email='an@example.com', age=0)
__repr__ wird automatisch generiert und zeigt den vollständigen Namen des Feldes und den Wert an — sehr praktisch beim Debuggen — normale Klassen haben das nicht standardmäßig.
3. Vergleich mit normalen Klassen und namedtuple
Eine manuell geschriebene äquivalente Klasse wäre viel länger:
class CustomerPlain:
def __init__(self, name, email, age=0):
self.name = name
self.email = email
self.age = age
def __repr__(self):
return f"CustomerPlain(name={self.name!r}, email={self.email!r}, age={self.age!r})"
def __eq__(self, other):
if not isinstance(other, CustomerPlain):
return NotImplemented
return (self.name, self.email, self.age) == (other.name, other.email, other.age)
@dataclass erspart Ihnen, all dieses zu schreiben. Im Vergleich zu namedtuple unterscheidet sich dataclass in folgenden Punkten:
- Objekte sind standardmäßig veränderbar (Sie können
c1.age = 20ändern), namedtuple dagegen nicht. - Unterstützt explizite Type-Hints und integriert sich leicht mit Type-Checkern wie
mypy. - Unterstützt
__post_init__, komplexe Standardwerte, Vererbung — Dinge, dienamedtuplenicht gut macht.
Hinweis: Wenn Sie nur ein leichtes unveränderliches Tuple ohne Methoden benötigen, ist
namedtupleodertyping.NamedTupleimmer noch eine knappere Wahl.
4. field() und veränderliche Standardwerte
Ein sehr häufiger Fehler: List/Dict direkt als Standardwert verwenden.
from dataclasses import dataclass, field
@dataclass
class Order:
order_id: str
items: list[str] = field(default_factory=list)
- Wenn Sie
items: list[str] = []schreiben, erhalten Sie eineValueError: mutable default ... is not allowed— weil der Standard-List zwischen allen Instanzen geteilt wird und zu schwer auffindbaren Bugs führt. field(default_factory=list)fordert dataclass auf,list()neu für jede Instanz aufzurufen.
o1 = Order("A1")
o2 = Order("A2")
o1.items.append("Sách")
print(o1.items, o2.items)
['Sách'] []
Die beiden Objekte haben unabhängige items, wie erwartet.
5. Daten mit __post_init__ validieren
Der __init__, den dataclass generiert, ermöglicht keine direkte Einfügung von Validierungslogik — aber es gibt einen Hook __post_init__, der direkt nach Abschluss von __init__ läuft.
from dataclasses import dataclass
@dataclass
class PaymentRequest:
amount: float
currency: str = "VND"
def __post_init__(self):
if self.amount <= 0:
raise ValueError("amount muss größer als 0 sein")
self.currency = self.currency.upper()
__post_init__wird automatisch aufgerufen, nachdem alle Felder zugewiesen sind.- Hier validieren wir
amountund normalisierencurrencyin Großbuchstaben.
try:
PaymentRequest(amount=-100)
except ValueError as e:
print(e)
amount muss größer als 0 sein
6. Unveränderliche Klasse mit frozen=True
Wenn Sie möchten, dass das Objekt nach der Erstellung nicht geändert werden kann (ähnlich wie ein Record in Java), verwenden Sie frozen=True.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
frozen=Trueblockiert__setattr__— jede Zuweisung nach der Initialisierung wirft einen Fehler.- Dank der Unveränderlichkeit können
frozen-Objekte als Schlüssel in einemdictoder Element in einemsetverwendet werden (da dataclass automatisch__hash__generiert, wennfrozen=Trueundeq=True).
p = Point(1.0, 2.0)
try:
p.x = 5.0
except Exception as e:
print(type(e).__name__, e)
FrozenInstanceError cannot assign to field 'x'
Hinweis:
frozen=Trueblockiert nur die Zuweisung auf oberster Ebene des Attributs. Wenn ein Feld vom Typ List/Dict ist, kann der Inhalt darin immer noch geändert werden. Für echte Unveränderlichkeit verwenden Sietupleoder einen unveränderlichen Typ für dieses Feld.
7. Dataclass-Vererbung
Dataclass unterstützt normale Python-Vererbung, Felder der abgeleiteten Klasse werden nach den Feldern der Basisklasse hinzugefügt.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
@dataclass
class Employee(Person):
salary: float = 0.0
Employeeerbtname,agevonPersonund fügtsalaryhinzu.- Die Reihenfolge der Parameter in dem generierten
__init__ist:name, age, salary— die Deklarationsreihenfolge wird beibehalten.
e = Employee("Bình", 30, salary=15_000_000)
print(e)
Employee(name='Bình', age=30, salary=15000000.0)
Hinweis: Felder ohne Standardwert müssen vor Feldern mit Standardwert in der gesamten Vererbungskette stehen. Wenn die Basisklasse ein Feld mit Standardwert hat und die abgeleitete Klasse ein Feld ohne Standardwert hinzufügt, wirft Python beim Definieren der Klasse einen
TypeError.
8. Vergleich und Sortierung mit order=True
Standardmäßig generiert dataclass nur __eq__. Um <, >, <= zu vergleichen, verwenden Sie order=True.
from dataclasses import dataclass, field
@dataclass(order=True)
class Product:
price: float
name: str = field(compare=False)
order=Truegeneriert zusätzlich__lt__,__le__,__gt__,__ge__, die Felder werden in der Reihenfolge verglichen (ähnlich wie Tuple-Vergleich).field(compare=False)schließtnameaus dem Vergleich aus — nur nachpricewird verglichen.
products = [Product(50_000, "Bút"), Product(20_000, "Vở")]
print(sorted(products))
[Product(price=20000, name='Vở'), Product(price=50000, name='Bút')]
sorted() verwendet __lt__, das von dataclass generiert wird, Sie brauchen nicht manuell key=lambda p: p.price zu schreiben.
9. Konvertierung zu dict und tuple
dataclasses bietet praktische Funktionen asdict und astuple zum Serialisieren von Objekten.
from dataclasses import dataclass, asdict, astuple
@dataclass
class Address:
city: str
country: str
@dataclass
class Customer2:
name: str
address: Address
cust = Customer2("Chi", Address("Hà Nội", "Việt Nam"))
print(asdict(cust))
print(astuple(cust))
{'name': 'Chi', 'address': {'city': 'Hà Nội', 'country': 'Việt Nam'}}
('Chi', ('Hà Nội', 'Việt Nam'))
asdictkonvertiert rekursiv auch verschachtelte dataclass (Address) in ein untergeordnetes dict — sehr praktisch für die JSON-Rückgabe über die API.astupleist ähnlich, gibt aber verschachtelte Tupel zurück.
Hinweis:
asdict/astupleerstellen eine vollständige Tiefenkopie der Daten. Bei großen Objekten oder solchen mit nicht kopierbaren Feldern (Datei-Handle, Verbindung…) ist der Aufruf dieser Funktionen langsam oder fehlerhaft.
10. Schreiben von Tests für dataclass
Da __eq__ automatisch basierend auf Feldwerten generiert wird, ist das Vergleichen von Objekten mit pytest sehr natürlich.
import pytest
from dataclasses import FrozenInstanceError
def test_customer_equality():
c1 = Customer("An", "an@example.com", 20)
c2 = Customer("An", "an@example.com", 20)
assert c1 == c2
def test_payment_request_rejects_non_positive_amount():
with pytest.raises(ValueError):
PaymentRequest(amount=0)
def test_point_is_frozen():
p = Point(1.0, 2.0)
with pytest.raises(FrozenInstanceError):
p.x = 10.0
test_customer_equalityprüft, dass zwei verschiedene Instanzen mit denselben Feldwerten sich==sind — stimmt dank automatisch generiertem__eq__.test_payment_request_rejects_non_positive_amountprüft, dass__post_init__korrekt validiert.test_point_is_frozenbestätigt, dassfrozen=TrueAttributzuweisungen blockiert.
11. Häufige Fehler und wann Sie dataclass nicht verwenden sollten
default_factoryfür veränderliche Standardwerte vergessen: Wenn Sie= []oder= {}direkt verwenden, erhalten Sie sofort einen Fehler beim Deklarieren der Klasse, Sie müssenfield(default_factory=...)verwenden.- Felder mit und ohne Standardwert in falscher Reihenfolge bei Vererbung: Ursacht
TypeErrorbeim Laden des Moduls, schwer zu debuggen, wenn Klassen in mehreren Dateien verteilt sind. - Annahme, dass
frozen=Truetiefe Unveränderlichkeit ist: Felder vom Typ List/Dict können ihren Inhalt immer noch ändern, nur die Attributzuweisung ist blockiert. - Dataclass für komplexe Geschäftslogik verwenden: Wenn eine Klasse viele Methoden zur Verarbeitung und komplexe Beziehungen zu anderen Objekten hat, hilft dataclass nur bei Initialisierung/Vergleich — Sie müssen die Logik immer noch selbst schreiben, machen Sie nicht vor, dass die Klasse einfacher ist als sie tatsächlich ist.
- Kompatibilität mit Python < 3.7 benötigen:
dataclassesist nicht in diesen Versionen enthalten, Sie müssen dasdataclasses-Paket von PyPI installieren (Backport) oderattrsverwenden.
Hinweis: Wenn Sie komplexe Validierung, benutzerdefinierte Serialisierung oder tiefe JSON-Schema-Integration benötigen, sind Bibliotheken wie
pydanticoft stärker als reinesdataclasses— es lohnt sich, diese zu berücksichtigen, wenn Ihr Projekt größer wird.
12. Fazit
dataclasses hilft Ihnen, den größten Teil des Boilerplate-Codes bei der Schreibweise von Datenklassen zu eliminieren: generiert automatisch __init__, __repr__, __eq__, unterstützt Validierung über __post_init__, Unveränderlichkeit über frozen=True und einfache Serialisierung über asdict/astuple. Für einfache bis mittlere Datenklassen ist dies eine prägnantere und lesbarere Wahl als manuelles Schreiben. Wenn die Anforderungen an Validierung oder Serialisierung komplexer werden, können Sie in Betracht ziehen, pydantic oder attrs als Alternative zu verwenden.