Dataclasses in Python: Weniger Boilerplate für deine Klassen
Mit Dataclasses schreibst du Klassen in Python deutlich kürzer – ganz ohne langen __init__-Code. Wir schauen uns an, wie sie funktionieren und wann sie sich wirklich lohnen.
Wenn du schon einmal eine Klasse geschrieben hast, die einfach nur ein paar Werte zusammenhalten soll, kennst du das Problem: Du tippst denselben langweiligen Code immer wieder. Erst die __init__-Methode, in der du jeden Wert von Hand zuweist, dann vielleicht noch eine __repr__ für die Ausgabe und eine __eq__ zum Vergleichen. Das ist viel Boilerplate – also Standardcode, der kaum etwas Eigenes ausdrückt. Seit Python 3.7 gibt es dafür eine elegante Lösung: Dataclasses. In diesem Beitrag schauen wir uns an, wie sie funktionieren und wann du sie einsetzen solltest.
Das Problem: Viel Code für wenig Inhalt
Stell dir vor, du möchtest einen Punkt im 2D-Raum modellieren. Mit einer klassischen Klasse sieht das so aus:
class Punkt:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Punkt(x={self.x}, y={self.y})"
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
p = Punkt(3, 4)
print(p) # Punkt(x=3, y=4)
print(p == Punkt(3, 4)) # TrueDas funktioniert, aber schau dir an, wie viel du tippen musstest – und das nur, um zwei Werte zu speichern. Bei größeren Klassen mit zehn Attributen wird das schnell unübersichtlich und fehleranfällig. Genau hier setzen Dataclasses an.
Die Lösung: Der @dataclass-Dekorator
Mit dem Dekorator @dataclass aus dem Modul dataclasses generiert Python die langweiligen Methoden automatisch für dich. Du beschreibst nur noch, welche Felder es gibt:
from dataclasses import dataclass
@dataclass
class Punkt:
x: int
y: int
p = Punkt(3, 4)
print(p) # Punkt(x=3, y=4)
print(p == Punkt(3, 4)) # TrueDerselbe Effekt, aber in einem Bruchteil der Zeilen. Python erzeugt im Hintergrund automatisch __init__, __repr__ und __eq__. Wichtig: Die Typ-Annotationen wie x: int sind hier Pflicht – ohne sie erkennt der Dekorator das Attribut nicht als Feld. Wenn du dich an dieser Stelle fragst, warum Typen überhaupt helfen, lohnt sich ein Blick auf das Thema Type Hints, das eng mit Dataclasses zusammenhängt.
Standardwerte und die Falle mit mutablen Defaults
Felder können Standardwerte haben, genau wie normale Funktionsparameter. Felder mit Default müssen dabei nach den Feldern ohne Default stehen:
from dataclasses import dataclass
@dataclass
class Benutzer:
name: str
aktiv: bool = True
rolle: str = "gast"
u = Benutzer("Anna")
print(u) # Benutzer(name='Anna', aktiv=True, rolle='gast')Vorsicht ist bei veränderbaren Standardwerten wie Listen oder Dictionaries geboten. Schreibst du tags: list = [], würden sich alle Instanzen dieselbe Liste teilen – ein klassischer Bug. Dataclasses verbieten das sogar und werfen einen Fehler. Die saubere Lösung ist field mit einer default_factory, die für jede Instanz ein frisches Objekt erzeugt:
from dataclasses import dataclass, field
@dataclass
class Einkaufswagen:
besitzer: str
artikel: list = field(default_factory=list)
a = Einkaufswagen("Tom")
b = Einkaufswagen("Lisa")
a.artikel.append("Kaffee")
print(a.artikel) # ['Kaffee']
print(b.artikel) # [] -> jede Instanz hat ihre eigene ListePraktische Extras: Sortieren und unveränderliche Objekte
Der @dataclass-Dekorator nimmt Parameter entgegen, mit denen du zusätzliches Verhalten aktivierst. Mit order=True generiert Python Vergleichsoperatoren, sodass du Objekte sortieren kannst. Verglichen wird dabei anhand der Felder in der Reihenfolge ihrer Definition:
from dataclasses import dataclass
@dataclass(order=True)
class Spieler:
punkte: int
name: str
spieler = [
Spieler(50, "Mia"),
Spieler(90, "Ben"),
Spieler(70, "Kim"),
]
bestenliste = sorted(spieler, reverse=True)
for s in bestenliste:
print(s.name, s.punkte)
# Ben 90
# Kim 70
# Mia 50Ein weiteres nützliches Feature ist frozen=True. Damit wird die Instanz unveränderlich: Nach dem Erstellen kannst du ihre Felder nicht mehr überschreiben. Das ist ideal für Werte, die sich nie ändern sollen, und macht deinen Code sicherer:
from dataclasses import dataclass
@dataclass(frozen=True)
class Konfiguration:
host: str
port: int
config = Konfiguration("localhost", 8080)
print(config.host) # localhost
config.port = 9090 # FrozenInstanceError: cannot assign to field 'port'Praktisch ist außerdem die Hilfsfunktion asdict, mit der du eine Dataclass schnell in ein Dictionary umwandelst – etwa um sie als JSON zu speichern:
from dataclasses import dataclass, asdict
@dataclass
class Konfiguration:
host: str
port: int
config = Konfiguration("localhost", 8080)
print(asdict(config)) # {'host': 'localhost', 'port': 8080}Wann solltest du Dataclasses verwenden?
Dataclasses sind kein Ersatz für jede Klasse, aber für einen bestimmten Typ glänzen sie. Greife zu ihnen, wenn:
- deine Klasse vor allem Daten bündelt, etwa eine Konfiguration, ein Messwert oder ein API-Ergebnis
- du automatische
__repr__- und__eq__-Methoden zu schätzen weißt, zum Beispiel beim Debugging oder in Tests - du unveränderliche Objekte (
frozen=True) oder sortierbare Objekte (order=True) brauchst
Hat deine Klasse dagegen viel komplexe Logik und kaum eigene Daten, ist eine gewöhnliche Klasse oft die bessere Wahl. Für reine, unveränderliche Wertepaare ohne Methoden kann auch ein NamedTuple passen – Dataclasses sind aber flexibler, sobald du Standardwerte oder Methoden ins Spiel bringst.
Fazit
Dataclasses nehmen dir das ständige Wiederholen von __init__, __repr__ und __eq__ ab und machen deinen Code dadurch kürzer, lesbarer und weniger fehleranfällig. Mit Optionen wie default_factory, order=True und frozen=True deckst du dabei erstaunlich viele Anwendungsfälle ab. Mein Tipp: Suche in deinem nächsten Projekt nach einer Klasse, die im Grunde nur Werte zusammenhält, und schreibe sie als Dataclass um. Du wirst überrascht sein, wie viele Zeilen du dabei loswirst – und wie viel angenehmer sich der Code danach liest.