Python-Dekoratoren einfach erklärt
Dekoratoren gehören zu den beliebtesten Sprachfeatures der Programmiersprache Python. Lerne, eigene Dekoratorenzu erstellen, um Boilerplate-Code zu reduzieren.
1. Das Problem: Boilerplate-Hölle
Wir wollen ein Haustier in Python abbilden — mit Name, Tierart und Alter. Das klingt einfach, aber eine saubere Klasse braucht überraschend viel Code.
Zunächst: Warum braucht man überhaupt diese ganzen Dunder-Methoden (__init__, __repr__, __eq__)?
__init__— der Konstruktor. Ohne ihn kann man keinHaustier("Balu", "Hund", 3)schreiben.__repr__— bestimmt, wasprint(balu)ausgibt. Ohne diese Methode sieht man nur etwas Kryptisches wie<__main__.Haustier object at 0x7f3b2c>— das hilft niemandem beim Debuggen.__eq__— bestimmt, was bei==passiert. Ohne diese Methode vergleicht Python nur, ob es dasselbe Objekt im Speicher ist, nicht ob die Werte gleich sind:
balu1 = Haustier("Balu", "Hund", 3)
balu2 = Haustier("Balu", "Hund", 3)
print(balu1 == balu2) # False! Gleiche Daten, aber zwei verschiedene Objekte
Um all das korrekt zu haben, muss man Folgendes schreiben:
class Haustier:
def __init__(self, name: str, tierart: str, alter: int):
self.name = name
self.tierart = tierart
self.alter = alter
def __repr__(self):
return f"Haustier(name='{self.name}', tierart='{self.tierart}', alter={self.alter})"
def __eq__(self, other):
if not isinstance(other, Haustier):
return NotImplemented
return (self.name == other.name
and self.tierart == other.tierart
and self.alter == other.alter)
Das sind 15 Zeilen — für drei Attribute. Und jedes Mal, wenn ein neues Attribut dazukommt (z. B. farbe), muss man an drei Stellen ändern: __init__, __repr__ und __eq__.
Können wir Python sagen: “Mach das für mich”?
2. Die Idee: Funktionen verändern Funktionen
In Python sind Funktionen Objekte. Man kann sie in Variablen speichern, an andere Funktionen übergeben und aus Funktionen zurückgeben. Das ermöglicht ein mächtiges Konzept: Dekoratoren.
Ein Dekorator ist eine Funktion, die eine andere Funktion entgegennimmt, sie erweitert und zurückgibt.
Beispiel: Der @laut-Dekorator
def laut(funktion):
def wrapper(*args, **kwargs):
ergebnis = funktion(*args, **kwargs)
return ergebnis.upper()
return wrapper
Ohne Dekorator-Schreibweise:
def gruesse(name):
return f"Hallo, {name}!"
gruesse = laut(gruesse)
print(gruesse("Anna")) # HALLO, ANNA!
Mit @-Schreibweise — syntaktischer Zucker für genau das Gleiche:
@laut
def gruesse(name):
return f"Hallo, {name}!"
print(gruesse("Anna")) # HALLO, ANNA!
Das @ vor der Funktion sagt: “Bevor du diese Funktion benutzt, schick sie zuerst durch laut hindurch.”
Dieses Prinzip funktioniert auch mit Klassen. Und genau das nutzt @dataclass.
3. @dataclass — Die Lösung
from dataclasses import dataclass
@dataclass
class Haustier:
name: str
tierart: str
alter: int
Das ist alles. Fünf Zeilen statt fünfzehn.
Der Dekorator @dataclass liest die Typ-Annotationen und generiert automatisch:
__init__— Konstruktor mit allen Feldern als Parameter__repr__— lesbare Ausgabe__eq__— Vergleich über alle Felder
Ausprobieren
balu = Haustier("Balu", "Hund", 3)
luna = Haustier("Luna", "Katze", 5)
balu2 = Haustier("Balu", "Hund", 3)
print(balu) # Haustier(name='Balu', tierart='Hund', alter=3)
print(balu == luna) # False
print(balu == balu2) # True
Wichtig zu wissen
Die Typ-Annotationen (str, int) sind keine Runtime-Checks. Python erzwingt sie nicht — sie dienen hier als Feld-Definitionen und Dokumentation. Das Folgende würde keinen Fehler werfen:
falsch = Haustier("Balu", "Hund", "drei") # Läuft ohne Fehler
4. Wichtige Optionen und Features
frozen=True — Unveränderliche Instanzen
@dataclass(frozen=True)
class Haustier:
name: str
tierart: str
alter: int
balu = Haustier("Balu", "Hund", 3)
balu.tierart = "Katze" # FrozenInstanceError!
Balu kann nicht plötzlich zur Katze werden.
field() mit default_factory — Sichere Standardwerte
Mutable Defaults (Listen, Dicts) direkt als Standardwert zu setzen ist in Python gefährlich, weil alle Instanzen dieselbe Liste teilen würden. field() löst das:
from dataclasses import dataclass, field
@dataclass
class Haustier:
name: str
tierart: str
alter: int
tricks: list = field(default_factory=list)
balu = Haustier("Balu", "Hund", 3)
balu.tricks.append("Sitz")
print(balu.tricks) # ['Sitz']
luna = Haustier("Luna", "Katze", 5)
print(luna.tricks) # [] — eigene leere Liste, nicht Balus Liste
__post_init__ — Validierung nach der Erstellung
@dataclass
class Haustier:
name: str
tierart: str
alter: int
def __post_init__(self):
if self.alter < 0:
raise ValueError(f"Alter kann nicht negativ sein: {self.alter}")
balu = Haustier("Balu", "Hund", -1) # ValueError!
5. Zusammenfassung
Das Problem: Klassen, die Daten halten, brauchen viel repetitiven Code.
Die Lösung: Der @dataclass-Dekorator generiert __init__, __repr__ und __eq__ automatisch aus den Feld-Definitionen.
Das Konzept dahinter: Dekoratoren sind Funktionen, die andere Funktionen oder Klassen erweitern — ohne deren Code zu verändern.
Wann @dataclass nutzen? Immer wenn eine Klasse hauptsächlich Daten hält: Konfigurationen, Ergebnisse, Zwischenspeicher, DTOs.
Alternativen: NamedTuple (unveränderlich, leichtgewichtig), attrs (mehr Features), Pydantic (mit Validierung zur Laufzeit).
Ähnliche Posts
Alle anzeigen
Python Closures: Funktionen mit Gedächtnis
Closures sind das Geheimnis hinter Decorators, Factories und elegantem State-Handling in Python. Ein praxisnaher Deep-Dive: wie sie funktionieren und wann du sie brauchst.
Werde Teil der Python-Community
Tausch dich mit anderen Python-Lernenden aus, stell deine Fragen im Forum und schau dir die besten Lernvideos rund um Python an — kostenlos auf Skool.