Python Dataclasses
Python Dataclasses erklärt: @dataclass-Dekorator, field()-Standardwerte, Sortierung, Unveränderlichkeit und Vererbung mit Beispielen.
Eine Dataclass ist eine reguläre Python-Klasse, deren Boilerplate — __init__, __repr__ und __eq__ — automatisch durch den @dataclass-Dekorator generiert wird. Das Ergebnis ist weniger Code, weniger Tippfehler und Klassen, die sofort lesbar sind.
Dieses Kapitel behandelt:
- Warum Dataclasses existieren und wann man sie verwendet
- Den
@dataclass-Dekorator - Standardwerte für Felder und den
field()-Helfer - Steuerung von Gleichheit und Sortierung
- Unveränderliche Dataclasses mit
frozen=True - Post-Initialisierungslogik mit
__post_init__ - Vererbung mit Dataclasses
- Dataclasses vs.
NamedTuplevs. einfachen Klassen
Bevor Sie dieses Kapitel lesen, sollten Sie sich mit Python-Klassen und -Objekten und Python-Vererbung vertraut gemacht haben.
Warum Dataclasses?
Stellen Sie sich eine Klasse vor, die ein Produkt in einem Online-Shop speichert. Ohne Dataclasses schreiben Sie die gleichen Attributzuweisungen dreimal — einmal in __init__, einmal in __repr__ und einmal in __eq__:
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price}, stock={self.stock})"
def __eq__(self, other):
if not isinstance(other, Product):
return NotImplemented
return (self.name, self.price, self.stock) == (other.name, other.price, other.stock)Der @dataclass-Dekorator generiert all das Obige aus einer einzigen annotierten Feldliste:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: intBeide Versionen verhalten sich identisch. Die Dataclass-Version ist kürzer, schwieriger falsch zu machen, und vermittelt sofort, dass diese Klasse in erster Linie ein Datenbehälter ist.
Der @dataclass-Dekorator
Importieren Sie dataclass aus dem Standardbibliotheksmodul dataclasses und wenden Sie es auf Ihre Klasse an. Jedes Feld wird als typannotierte Klassenvariable deklariert:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.5, 2.0)
print(p) # Point(x=1.5, y=2.0)
print(p.x) # 1.5
p2 = Point(1.5, 2.0)
print(p == p2) # True — __eq__ compares field by fieldDer Dekorator generiert:
| Methode | Was sie tut |
|---|---|
__init__ | Akzeptiert jedes Feld als Parameter und weist es self zu |
__repr__ | Gibt einen lesbaren string zurück, wie z. B. Point(x=1.5, y=2.0) |
__eq__ | Vergleicht zwei Instanzen Feld für Feld |
Typannotationen sind erforderlich, werden aber zur Laufzeit nicht durchgesetzt
Felddeklarationen erfordern eine Typannotation (x: float). Python überprüft den Typ zur Laufzeit nicht — Sie können trotzdem einen string übergeben, wo ein float erwartet wird. Die Annotation ist Metadaten, die von Typprüfern wie mypy und vom dataclasses-Mechanismus selbst verwendet werden. Zur Laufzeit-Typvalidierung siehe Python Type Hints.
Standardwerte
Weisen Sie einem Feld direkt einen Standardwert zu, um es in __init__ optional zu machen:
from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
c1 = Config()
print(c1) # Config(host='localhost', port=8080, debug=False)
c2 = Config(host="example.com", port=443)
print(c2) # Config(host='example.com', port=443, debug=False)Felder mit Standardwerten müssen nach Feldern ohne Standardwerte stehen — genau dieselbe Regel wie bei regulären Funktionsparametern.
Veränderliche Standardwerte und field()
Sie können kein veränderbares object (eine Liste, ein dict oder ein set) als einfachen Standardwert verwenden. Python würde eine Liste unter allen Instanzen teilen, was zu subtilen Fehlern führt:
from dataclasses import dataclass
# This raises a ValueError at class definition time:
# @dataclass
# class Bag:
# items: list = [] # ValueError: mutable default is not allowedVerwenden Sie stattdessen field(default_factory=...), um für jede Instanz ein frisches object zu erstellen:
from dataclasses import dataclass, field
@dataclass
class Bag:
items: list = field(default_factory=list)
b1 = Bag()
b2 = Bag()
b1.items.append("apple")
print(b1.items) # ['apple']
print(b2.items) # [] — b2 has its own separate listdefault_factory akzeptiert jeden nullargumentigen Callable, einschließlich Lambdas und eigener Funktionen.
Der field()-Helfer
field() gibt Ihnen feingranulare Kontrolle über einzelne Felder. Die nützlichsten Parameter sind:
| Parameter | Zweck |
|---|---|
default | Ein einfacher Standardwert (nur skalare Typen) |
default_factory | Ein Callable, der den Standardwert erzeugt |
repr | False, um dieses Feld aus __repr__ auszuschließen |
compare | False, um dieses Feld aus __eq__ (und der Sortierung) auszuschließen |
init | False, um dieses Feld aus __init__ auszuschließen |
from dataclasses import dataclass, field
import time
@dataclass
class LogEntry:
message: str
level: str = "INFO"
timestamp: float = field(default_factory=time.time, repr=False, compare=False)
entry = LogEntry("Server started")
print(entry) # LogEntry(message='Server started', level='INFO')
# timestamp exists but is hidden from repr and ignored in comparisons
print(entry.timestamp > 0) # TrueSortierung
Standardmäßig unterstützen Dataclasses Gleichheit (==, !=), aber keine Sortierung (<, >, <=, >=). Aktivieren Sie die Sortierung, indem Sie order=True an den Dekorator übergeben:
from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
v1 = Version(1, 2, 0)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 0)
print(v1 < v2) # True
print(v1 == v3) # True
print(v2 > v1) # True
versions = [Version(2, 0, 0), Version(1, 9, 1), Version(1, 2, 3)]
print(sorted(versions))
# [Version(major=1, minor=2, patch=3),
# Version(major=1, minor=9, patch=1),
# Version(major=2, minor=0, patch=0)]Python generiert die Vergleichsmethoden, indem es die Felder in der Reihenfolge ihrer Deklaration tupel-artig vergleicht. Sie können ein Feld mit field(compare=False) von Vergleichen ausschließen.
Unveränderliche Dataclasses mit frozen=True
Übergeben Sie frozen=True, um alle Felder nach der Erstellung schreibgeschützt zu machen. Jeder Versuch, ein Feld zu ändern, löst einen FrozenInstanceError aus:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
london = Coordinate(51.5074, -0.1278)
print(london) # Coordinate(lat=51.5074, lon=-0.1278)
# london.lat = 0.0 # FrozenInstanceError: cannot assign to field 'lat'Eingefrorene Dataclasses sind auch hashbar (sie implementieren __hash__), sodass Sie sie als Dictionary-Schlüssel oder Mitglieder von Sets verwenden können:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
cities = {
Coordinate(51.5074, -0.1278): "London",
Coordinate(48.8566, 2.3522): "Paris",
}
print(cities[Coordinate(51.5074, -0.1278)]) # LondonReguläre (veränderliche) Dataclasses sind standardmäßig nicht hashbar — Python setzt __hash__ auf None, wenn __eq__ ohne frozen=True definiert wird.
Post-Initialisierungslogik mit __post_init__
Manchmal müssen Sie den Wert eines Feldes aus anderen Feldern ableiten oder die Eingabe nach dem Ausführen von __init__ validieren. Definieren Sie eine __post_init__-Methode — sie wird automatisch am Ende des generierten __init__ aufgerufen:
from dataclasses import dataclass, field
import math
@dataclass
class Circle:
radius: float
def __post_init__(self):
if self.radius <= 0:
raise ValueError(f"radius must be positive, got {self.radius}")
@property
def area(self):
return math.pi * self.radius ** 2
c = Circle(5)
print(round(c.area, 4)) # 78.5398
# Circle(-1) # ValueError: radius must be positive, got -1Sie können auch ein abgeleitetes Feld berechnen. Markieren Sie es mit field(init=False), damit es nicht in __init__ erscheint, und setzen Sie es dann innerhalb von __post_init__:
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False, repr=True)
def __post_init__(self):
self.area = self.width * self.height
r = Rectangle(4, 6)
print(r) # Rectangle(width=4, height=6, area=24)
print(r.area) # 24Vererbung mit Dataclasses
Eine Dataclass kann von einer anderen Dataclass erben. Das __init__ der Kindklasse enthält Felder aus beiden Klassen — Elternfelder zuerst, in der Reihenfolge ihrer Deklaration:
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
rex = Dog(name="Rex", age=3, breed="Labrador")
print(rex) # Dog(name='Rex', age=3, breed='Labrador')Achtung: Wenn eine Elternklasse ein Feld mit einem Standardwert hat, müssen alle Kindfelder ebenfalls Standardwerte haben. Dies ist dieselbe Regel, die für reguläre Python-Funktionssignaturen gilt — ein Parameter ohne Standardwert kann keinem mit Standardwert folgen.
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int = 0 # has a default
# @dataclass
# class Dog(Animal):
# breed: str # TypeError: non-default argument 'breed' follows default argumentUmgehen Sie dies, indem Sie dem Kindfeld ebenfalls einen Standardwert geben oder die Hierarchie so umstrukturieren, dass Felder mit Standardwerten zuletzt kommen.
Dekoratorparameter auf einen Blick
@dataclass(
init=True, # generate __init__ (default True)
repr=True, # generate __repr__ (default True)
eq=True, # generate __eq__ (default True)
order=False, # generate <, >, <=, >= (default False)
frozen=False, # make fields immutable (default False)
)
class MyClass:
...In den meisten Fällen müssen Sie die wenigsten dieser Parameter ändern. Die gängigsten sind order=True und frozen=True.
Hilfsfunktionen
Das dataclasses-Modul bietet auch drei praktische Funktionen:
fields()
Gibt ein Tupel von Field-Objekten zurück, die jedes Feld in der Klasse beschreiben:
from dataclasses import dataclass, fields
@dataclass
class Point:
x: float
y: float
for f in fields(Point):
print(f.name, f.type)
# x <class 'float'>
# y <class 'float'>asdict()
Konvertiert eine Dataclass-Instanz in ein einfaches Dictionary (rekursiv):
from dataclasses import dataclass, asdict
@dataclass
class Address:
street: str
city: str
@dataclass
class Person:
name: str
address: Address
p = Person("Alice", Address("10 Downing St", "London"))
print(asdict(p))
# {'name': 'Alice', 'address': {'street': '10 Downing St', 'city': 'London'}}Dies ist nützlich beim Serialisieren in JSON oder beim Senden von Daten an eine API.
astuple()
Konvertiert in ein Tupel (rekursiv):
from dataclasses import dataclass, astuple
@dataclass
class Point:
x: float
y: float
p = Point(3.0, 4.0)
print(astuple(p)) # (3.0, 4.0)Dataclasses vs. NamedTuple vs. einfache Klassen
| Merkmal | Einfache Klasse | NamedTuple | dataclass |
|---|---|---|---|
Auto __init__ | Nein | Ja | Ja |
Auto __repr__ | Nein | Ja | Ja |
Auto __eq__ | Nein | Ja (nach Wert) | Ja (nach Wert) |
| Veränderlich | Ja | Nein | Ja (Standard) |
| Hashbar | Nein (wenn __eq__ definiert) | Ja | Nur mit frozen=True |
| Sortierung | Manuell | Ja | order=True |
| Vererbung | Ja | Eingeschränkt | Ja |
isinstance-Prüfung | Ja | Ja (auch tuple) | Ja |
Entpacken (a, b = obj) | Nein | Ja | Nein |
Verwenden Sie eine Dataclass, wenn:
- Sie veränderliche Daten mit optionaler Unveränderlichkeit möchten.
- Sie Vererbung oder Post-Init-Logik benötigen.
- Sie feingranulare Feldkontrolle wünschen (
field()).
Verwenden Sie NamedTuple, wenn:
- Sie einen unveränderlichen Datensatz wünschen, der sich auch wie ein Tupel verhält (positionelles Entpacken, CSV-Zeilen).
- Sie Kompatibilität mit Code benötigen, der Tupel erwartet.
Verwenden Sie eine einfache Klasse, wenn:
- Die Klasse erhebliches Verhalten und sehr wenige reine Daten hat.
- Sie ein benutzerdefiniertes
__init__benötigen, das sich nicht durch__post_init__ausdrücken lässt.
Häufige Fallstricke
Veränderliche Standardwerte. Die Verwendung einer Liste oder eines dict als einfachen Standardwert löst zur Klassendefinitionszeit einen ValueError aus. Verwenden Sie immer field(default_factory=...).
Hashbarkeit. Reguläre Dataclasses sind nicht hashbar. Wenn Sie sie als dict-Schlüssel oder in Sets benötigen, verwenden Sie frozen=True oder übergeben Sie unsafe_hash=True (selten empfohlen).
eq=False. Wenn Sie die Gleichheitsgenerierung deaktivieren (eq=False), greift Python auf den Identitätsvergleich (is) zurück, was für Datenobjekte fast nie das Gewünschte ist.
Reihenfolge der geerbten Standardwerte. Wenn ein Elternfeld einen Standardwert hat und ein Kindfeld keinen, löst Python einen TypeError aus. Planen Sie die Feldreihenfolge in Ihrer Hierarchie sorgfältig.
Zusammenfassung
| Konzept | Was es tut |
|---|---|
@dataclass | Generiert __init__, __repr__, __eq__ automatisch |
field() | Feingranulare Feldkontrolle: Standardwerte, repr, compare, init |
default_factory | Liefert einen frischen veränderbaren Standardwert für jede Instanz |
order=True | Fügt <, >, <=, >= basierend auf der Feldreihenfolge hinzu |
frozen=True | Macht Felder schreibgeschützt und die Instanz hashbar |
__post_init__ | Wird nach __init__ für Validierung oder abgeleitete Felder ausgeführt |
fields() | Gibt Metadaten über jedes Feld zurück |
asdict() | Konvertiert eine Instanz in ein einfaches dict (rekursiv) |
astuple() | Konvertiert eine Instanz in ein einfaches Tupel (rekursiv) |
Verwandte Themen finden Sie unter Python-Klassen und -Objekte, Python-Vererbung und abstrakte Python-Basisklassen.