cat posts/decorators-entmystifiziert.md
Decorators entmystifiziert
Was hinter dem @ steckt: Funktionen sind Objekte, Closures verpacken Verhalten, functools.wraps erhält Metadaten – und Decorators werden damit alltagstauglich.
In Teil 11 der Python-Serie
sind Decorators kurz aufgetaucht: diese @-Zeilen über Funktionen und Klassen.
Sie wirken zunächst magisch:
@mit_protokoll
def speichern(name):
...
Oder:
@dataclass
class Gegenstand:
...
Oder in Webframeworks:
@app.get("/status")
def status():
...
Decorators sind aber keine Magie. Hinter ihnen stecken vor allem drei Ideen:
- Funktionen sind Objekte.
- Funktionen können andere Funktionen entgegennehmen und zurückgeben.
@decoratorist Kurzform für eine Neuzuweisung.
Wenn Du diese drei Punkte verstanden hast, werden Decorators deutlich weniger mysteriös.
Funktionen sind Objekte
Eine Funktion ist in Python ein Objekt.
Du kannst sie einer Variablen zuweisen:
def begruesse():
return "Hallo"
funktion = begruesse
print(funktion())
Ausgabe:
Hallo
Wichtig ist der Unterschied zwischen:
begruesse
und:
begruesse()
begruesse meint die Funktion selbst.
begruesse() ruft die Funktion auf und liefert ihr Ergebnis.
print(begruesse)
print(begruesse())
Ausgabe ungefähr:
<function begruesse at 0x...>
Hallo
Genau weil Funktionen Objekte sind, kannst Du sie weiterreichen.
Funktionen als Argumente übergeben
Eine Funktion kann eine andere Funktion als Argument bekommen:
def begruesse(name):
return f"Hallo, {name}!"
def rufe_auf(funktion, wert):
ergebnis = funktion(wert)
print(ergebnis)
rufe_auf(begruesse, "Karl")
Ausgabe:
Hallo, Karl!
rufe_auf(...) weiß nicht, welche Funktion sie bekommt. Sie ruft einfach das
übergebene Funktionsobjekt auf.
Das kennst Du indirekt schon von sorted(...):
namen = ["Karl", "Alexandra", "Mia"]
sortiert = sorted(namen, key=len)
print(sortiert)
Ausgabe:
['Mia', 'Karl', 'Alexandra']
len wird hier als Funktion übergeben. sorted(...) ruft sie später für die
einzelnen Werte auf.
Funktionen können Funktionen zurückgeben
Eine Funktion kann auch eine neue Funktion erzeugen und zurückgeben.
def baue_begruessung():
def begruesse(name):
return f"Hallo, {name}!"
return begruesse
Verwendung:
funktion = baue_begruessung()
print(funktion("Karl"))
Ausgabe:
Hallo, Karl!
Die innere Funktion begruesse(...) entsteht beim Aufruf von
baue_begruessung(...) und wird anschließend zurückgegeben.
Damit sind wir schon sehr nah an Decorators.
Eine Funktion einwickeln
Ein Decorator ist im Kern eine Funktion, die eine andere Funktion entgegennimmt und eine neue Funktion zurückgibt.
def mit_protokoll(funktion):
def wrapper():
print(f"[log] {funktion.__name__} startet")
ergebnis = funktion()
print(f"[log] {funktion.__name__} fertig")
return ergebnis
return wrapper
Eine normale Funktion:
def speichern():
print("Spielstand gespeichert.")
Jetzt wickeln wir sie von Hand ein:
speichern = mit_protokoll(speichern)
Danach:
speichern()
Ausgabe:
[log] speichern startet
Spielstand gespeichert.
[log] speichern fertig
Der Name speichern zeigt jetzt nicht mehr auf die ursprüngliche Funktion,
sondern auf den wrapper.
Der wrapper ruft innen die ursprüngliche Funktion auf und ergänzt Verhalten
davor und danach.
Das ist das Grundprinzip eines Decorators.
Das @ ist nur Kurzform
Diese Schreibweise:
@mit_protokoll
def speichern():
print("Spielstand gespeichert.")
ist Kurzform für:
def speichern():
print("Spielstand gespeichert.")
speichern = mit_protokoll(speichern)
Mehr steckt im einfachen Fall nicht hinter der @-Syntax.
Der Decorator wird direkt nach der Funktionsdefinition angewendet. Der Name der Funktion wird anschließend an das Ergebnis des Decorators gebunden.
Das bedeutet:
speichern()
ruft später nicht mehr direkt die ursprüngliche Funktion auf, sondern die eingewickelte Version.
Argumente weiterreichen mit *args und **kwargs
Unser erster Decorator funktioniert nur für Funktionen ohne Argumente.
Diese Funktion hätte ein Problem:
def speichern(name):
print(f"{name} gespeichert.")
Der bisherige wrapper() nimmt keine Argumente entgegen.
Damit ein Decorator allgemein verwendbar ist, nutzt man *args und **kwargs:
def mit_protokoll(funktion):
def wrapper(*args, **kwargs):
print(f"[log] {funktion.__name__} startet")
ergebnis = funktion(*args, **kwargs)
print(f"[log] {funktion.__name__} fertig")
return ergebnis
return wrapper
Jetzt funktioniert:
@mit_protokoll
def speichern(name):
print(f"{name} gespeichert.")
speichern("Karl")
Ausgabe:
[log] speichern startet
Karl gespeichert.
[log] speichern fertig
*args sammelt positionale Argumente.
**kwargs sammelt Keyword Arguments.
Der wrapper nimmt alles entgegen und reicht es unverändert an die ursprüngliche
Funktion weiter.
Rückgabewerte nicht verlieren
Ein häufiger Fehler beim Schreiben eigener Decorators ist, den Rückgabewert der ursprünglichen Funktion zu vergessen.
Schlecht:
def mit_protokoll(funktion):
def wrapper(*args, **kwargs):
print("Starte")
funktion(*args, **kwargs)
print("Fertig")
return wrapper
Wenn die dekorierte Funktion etwas zurückgibt, geht dieser Wert verloren.
@mit_protokoll
def addiere(a, b):
return a + b
ergebnis = addiere(2, 3)
print(ergebnis)
Ausgabe:
Starte
Fertig
None
Besser:
def mit_protokoll(funktion):
def wrapper(*args, **kwargs):
print("Starte")
ergebnis = funktion(*args, **kwargs)
print("Fertig")
return ergebnis
return wrapper
Jetzt bleibt der Rückgabewert erhalten:
Starte
Fertig
5
Faustregel:
Wenn der Decorator den Rückgabewert nicht bewusst verändern soll, gib ihn unverändert zurück.
Closures: die Funktion erinnert sich
Der wrapper verwendet die Variable funktion:
def mit_protokoll(funktion):
def wrapper(*args, **kwargs):
return funktion(*args, **kwargs)
return wrapper
Obwohl mit_protokoll(...) längst fertig ausgeführt ist, weiß wrapper später
noch, welche Funktion gemeint war.
Das nennt man eine Closure.
Eine Closure ist eine innere Funktion, die sich Variablen aus ihrem umgebenden Scope merkt.
Kleines Beispiel:
def multiplizierer(faktor):
def rechne(zahl):
return zahl * faktor
return rechne
Verwendung:
verdopple = multiplizierer(2)
verdreifache = multiplizierer(3)
print(verdopple(10))
print(verdreifache(10))
Ausgabe:
20
30
rechne(...) merkt sich jeweils den passenden faktor.
Decorators nutzen genau dieses Prinzip: Der wrapper merkt sich die ursprüngliche
Funktion.
functools.wraps nicht vergessen
Unser Decorator funktioniert, hat aber ein Problem.
def mit_protokoll(funktion):
def wrapper(*args, **kwargs):
return funktion(*args, **kwargs)
return wrapper
@mit_protokoll
def speichern(name):
"""Speichert den Spielstand."""
print(f"{name} gespeichert.")
Von außen sieht die Funktion nun so aus:
print(speichern.__name__)
print(speichern.__doc__)
Ausgabe:
wrapper
None
Die dekorierte Funktion heißt scheinbar wrapper. Ihr Docstring ist verloren.
Das ist schlecht für:
- Debugging,
- Tracebacks,
- Dokumentation,
- CLI-Frameworks,
- Tests,
- Introspection,
- Type Checker und Editor-Hilfe.
Darum verwendet man functools.wraps:
from functools import wraps
def mit_protokoll(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
return funktion(*args, **kwargs)
return wrapper
Nun bleiben wichtige Metadaten erhalten:
@mit_protokoll
def speichern(name):
"""Speichert den Spielstand."""
print(f"{name} gespeichert.")
print(speichern.__name__)
print(speichern.__doc__)
Ausgabe:
speichern
Speichert den Spielstand.
Faustregel:
In jeden selbst geschriebenen Function Decorator gehört
@functools.wrapsan denwrapper.
Ein vollständiger Logging-Decorator
Ein alltagstauglicher Decorator sieht also eher so aus:
from functools import wraps
def mit_protokoll(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print(f"[log] {funktion.__name__} startet")
ergebnis = funktion(*args, **kwargs)
print(f"[log] {funktion.__name__} fertig")
return ergebnis
return wrapper
Verwendung:
@mit_protokoll
def speichern(name):
print(f"{name} gespeichert.")
return True
erfolg = speichern("Karl")
print(erfolg)
Ausgabe:
[log] speichern startet
Karl gespeichert.
[log] speichern fertig
True
Für echtes Logging würdest Du statt print(...) natürlich das
logging-Modul verwenden.
import logging
from functools import wraps
logger = logging.getLogger(__name__)
def mit_protokoll(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
logger.info("%s startet", funktion.__name__)
ergebnis = funktion(*args, **kwargs)
logger.info("%s fertig", funktion.__name__)
return ergebnis
return wrapper
Exceptions im Decorator
Ein Decorator kann auch Fehler protokollieren.
import logging
from functools import wraps
logger = logging.getLogger(__name__)
def logge_fehler(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
try:
return funktion(*args, **kwargs)
except Exception:
logger.exception("%s ist fehlgeschlagen", funktion.__name__)
raise
return wrapper
Wichtig ist das abschließende:
raise
Damit wird die ursprüngliche Exception erneut ausgelöst.
Der Decorator loggt also den Fehler, verschluckt ihn aber nicht.
Schlecht wäre:
except Exception:
logger.exception("Fehler")
return None
Das kann Bugs verstecken. Plötzlich sieht der Aufrufer nur None, obwohl
eigentlich etwas kaputtgegangen ist.
Wenn ein Decorator Exceptions behandelt, sollte das sehr bewusst passieren.
Laufzeit messen
Ein praktischer Decorator misst die Laufzeit einer Funktion:
from functools import wraps
from time import perf_counter
def misst_laufzeit(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
return funktion(*args, **kwargs)
finally:
ende = perf_counter()
dauer = ende - start
print(f"{funktion.__name__} dauerte {dauer:.3f}s")
return wrapper
Verwendung:
@misst_laufzeit
def lade_raeume():
return ["halle", "bibliothek", "krypta"]
raeume = lade_raeume()
finally sorgt dafür, dass die Zeitmessung auch dann ausgegeben wird, wenn die
Funktion eine Exception auslöst.
Das kann beim Debugging nützlich sein. Für Produktionscode wäre wieder Logging
statt print(...) sinnvoller.
Decorators mit Argumenten
Manchmal soll der Decorator selbst konfigurierbar sein:
@wiederhole(3)
def klopf():
print("klopf")
Das sieht so aus, als würde wiederhole(3) direkt die Funktion dekorieren.
Tatsächlich brauchen wir eine zusätzliche Ebene.
from functools import wraps
def wiederhole(mal):
def decorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
ergebnis = None
for _ in range(mal):
ergebnis = funktion(*args, **kwargs)
return ergebnis
return wrapper
return decorator
Verwendung:
@wiederhole(3)
def klopf():
print("klopf")
klopf()
Ausgabe:
klopf
klopf
klopf
Die Ebenen sind:
wiederhole(3)wird aufgerufen.- Es entsteht ein Decorator.
- Dieser Decorator bekommt die Funktion.
- Er gibt den
wrapperzurück.
Die Langform wäre:
def klopf():
print("klopf")
decorator = wiederhole(3)
klopf = decorator(klopf)
Oder in einer Zeile:
klopf = wiederhole(3)(klopf)
Ein Decorator mit Optionen
Ein realistischeres Beispiel: ein Decorator, der eine Funktion bei Fehlern erneut aufruft.
from functools import wraps
def wiederholen_bei_fehler(versuche):
if versuche < 1:
raise ValueError("versuche muss mindestens 1 sein")
def decorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
letzter_fehler = None
for _ in range(versuche):
try:
return funktion(*args, **kwargs)
except OSError as fehler:
letzter_fehler = fehler
raise letzter_fehler
return wrapper
return decorator
Verwendung:
@wiederholen_bei_fehler(3)
def lade_datei(pfad):
with open(pfad, encoding="utf-8") as datei:
return datei.read()
Der Decorator versucht die Funktion bis zu dreimal, aber nur bei OSError.
Das ist bewusst eingeschränkt. Ein Decorator, der einfach jede Exception schluckt und blind wiederholt, kann echte Programmierfehler verdecken.
Mehrere Decorators übereinander
Du kannst mehrere Decorators auf dieselbe Funktion setzen:
@decorator_a
@decorator_b
def funktion():
...
Die Anwendung erfolgt von unten nach oben.
Das entspricht:
def funktion():
...
funktion = decorator_a(decorator_b(funktion))
Der untere Decorator wird also zuerst auf die ursprüngliche Funktion angewendet. Der obere Decorator bekommt das Ergebnis des unteren.
Ein Beispiel:
from functools import wraps
def a(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print("a davor")
ergebnis = funktion(*args, **kwargs)
print("a danach")
return ergebnis
return wrapper
def b(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print("b davor")
ergebnis = funktion(*args, **kwargs)
print("b danach")
return ergebnis
return wrapper
@a
@b
def hallo():
print("hallo")
hallo()
Ausgabe:
a davor
b davor
hallo
b danach
a danach
b umschließt zuerst die ursprüngliche Funktion. Danach umschließt a das
Ergebnis.
Die Reihenfolge kann wichtig sein, zum Beispiel bei Authentifizierung, Caching, Logging oder Transaktionen.
State im Decorator
Closures können State speichern.
Beispiel: Zählen, wie oft eine Funktion aufgerufen wurde.
from functools import wraps
def zaehle_aufrufe(funktion):
anzahl = 0
@wraps(funktion)
def wrapper(*args, **kwargs):
nonlocal anzahl
anzahl += 1
print(f"{funktion.__name__} wurde {anzahl}x aufgerufen")
return funktion(*args, **kwargs)
return wrapper
Verwendung:
@zaehle_aufrufe
def begruesse(name):
print(f"Hallo, {name}!")
begruesse("Karl")
begruesse("Lara")
Ausgabe:
begruesse wurde 1x aufgerufen
Hallo, Karl!
begruesse wurde 2x aufgerufen
Hallo, Lara!
Dieser State gehört zu genau dieser dekorierten Funktion.
Dekorierst Du eine zweite Funktion, bekommt sie ihren eigenen Closure-State.
Das kann nützlich sein. Es kann aber auch überraschen, wenn der Decorator mutable Daten speichert und mehrere Aufrufe beeinflusst.
Für einfache Logging- oder Timing-Decorators brauchst Du normalerweise keinen veränderlichen State.
Caching mit functools.cache
Ein sehr praktischer Decorator aus der Standardbibliothek ist functools.cache.
from functools import cache
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
Ohne Cache berechnet eine naive rekursive Fibonacci-Funktion dieselben Werte ständig neu.
Mit @cache merkt sich Python bereits berechnete Ergebnisse.
print(fibonacci(40))
Das ist deutlich schneller als die ungebremste rekursive Variante.
Wichtig: Die Argumente müssen hashable sein, weil der Cache sie als Schlüssel verwendet.
Gut:
fibonacci(40)
Problematisch:
@cache
def verarbeite_liste(werte):
return sum(werte)
verarbeite_liste([1, 2, 3])
Eine Liste ist nicht hashable. Dafür wäre ein Tupel geeignet:
verarbeite_liste((1, 2, 3))
lru_cache: begrenzter Cache
functools.lru_cache ist die ältere und flexiblere Variante.
from functools import lru_cache
@lru_cache(maxsize=128)
def berechne(zahl):
return zahl * zahl
maxsize=128 bedeutet: Es werden bis zu 128 Ergebnisse gespeichert.
cache ist im Wesentlichen die einfache unbeschränkte Variante.
Orientierung:
@cache: simpel, unbeschränkt, gut für kleine Argumenträume.@lru_cache(maxsize=...): begrenzt, besser kontrollierbar.
Bei langlebigen Programmen solltest Du unbeschränkte Caches vorsichtig einsetzen. Sie können wachsen, wenn immer neue Argumente verwendet werden.
@property: Methoden wie Attribute verwenden
Ein weiterer häufiger Decorator ist @property.
class Spieler:
def __init__(self, name, hp, max_hp):
self.name = name
self.hp = hp
self.max_hp = max_hp
@property
def ist_besiegt(self):
return self.hp <= 0
Verwendung:
spieler = Spieler("Karl", hp=0, max_hp=100)
print(spieler.ist_besiegt)
Ausgabe:
True
Ohne @property müsstest Du schreiben:
spieler.ist_besiegt()
Mit @property wirkt die Methode wie ein Attribut.
Das passt gut für berechnete Eigenschaften, die keine sichtbaren Nebenwirkungen haben.
Weniger gut:
@property
def lade_datei(self):
...
Eine Property sollte nicht überraschend teure oder verändernde Aktionen ausführen.
Setter für Properties
Properties können auch Setter haben.
class Spieler:
def __init__(self, name, hp=100):
self.name = name
self._hp = 0
self.hp = hp
@property
def hp(self):
return self._hp
@hp.setter
def hp(self, wert):
if wert < 0:
wert = 0
self._hp = wert
Verwendung:
spieler = Spieler("Karl")
spieler.hp = -50
print(spieler.hp)
Ausgabe:
0
Der Setter fängt negative Werte ab.
Das kann sinnvoll sein. Trotzdem sollte man Properties nicht überstrapazieren. Wenn eine Operation fachlich etwas tut, ist eine Methode oft klarer:
spieler.erleide_schaden(50)
ist aussagekräftiger als eine versteckte Berechnung in einem Setter.
@staticmethod und @classmethod
Auch staticmethod und classmethod sind Decorators.
@staticmethod
Eine Static Method lebt im Namensraum der Klasse, bekommt aber weder self noch
cls.
class Wuerfel:
@staticmethod
def ist_gueltige_seitenzahl(seiten):
return seiten >= 2
Aufruf:
print(Wuerfel.ist_gueltige_seitenzahl(6))
Ausgabe:
True
Das ist im Grunde eine normale Funktion, die logisch zur Klasse gehört.
Wenn sie weder Objekt noch Klasse braucht, kann sie aber oft auch einfach eine Modulfunktion sein.
@classmethod
Eine Class Method bekommt die Klasse als erstes Argument, üblicherweise cls.
class Spieler:
def __init__(self, name, hp):
self.name = name
self.hp = hp
@classmethod
def aus_dict(cls, daten):
return cls(
name=daten["name"],
hp=daten["hp"],
)
Verwendung:
daten = {
"name": "Karl",
"hp": 100,
}
spieler = Spieler.aus_dict(daten)
print(spieler.name)
Ausgabe:
Karl
@classmethod ist besonders nützlich für alternative Konstruktoren.
@dataclass: ein Class Decorator
Decorators können nicht nur Funktionen dekorieren, sondern auch Klassen.
Ein sehr bekanntes Beispiel ist @dataclass:
from dataclasses import dataclass
@dataclass
class Gegenstand:
name: str
beschreibung: str = ""
Der Decorator ergänzt Methoden wie __init__, __repr__ und __eq__ anhand der
annotierten Fields.
Das ist keine Magie. @dataclass nimmt die Klasse entgegen, bearbeitet sie und
gibt sie zurück.
Vereinfacht entspricht:
@dataclass
class Gegenstand:
...
der Idee:
class Gegenstand:
...
Gegenstand = dataclass(Gegenstand)
Natürlich macht dataclass(...) intern deutlich mehr, aber das Decorator-Prinzip
ist dasselbe.
Decorators in Frameworks
Viele Frameworks verwenden Decorators, um Funktionen zu registrieren.
Ein typisches Muster in Webframeworks:
@app.get("/status")
def status():
return {"ok": True}
Oder bei Click:
@click.command()
@click.option("--name")
def begruesse(name):
click.echo(f"Hallo, {name}")
Oder bei pytest:
@pytest.fixture
def spieler():
return Spieler("Test")
Der Decorator verändert hier nicht immer nur den Funktionsaufruf.
Manchmal registriert er die Funktion auch irgendwo:
- als Route,
- als CLI-Kommando,
- als Test-Fixture,
- als Signal-Handler,
- als Task,
- als Plugin.
Das Prinzip bleibt:
Eine Funktion oder Klasse wird an einen Decorator übergeben. Der Decorator gibt etwas zurück oder registriert nebenbei zusätzliche Informationen.
Decorator oder normale Funktion?
Nur weil Decorators möglich sind, sind sie nicht immer die beste Lösung.
Ein Decorator ist gut, wenn dasselbe Verhalten viele Funktionen umgeben soll:
- Logging,
- Timing,
- Caching,
- Berechtigungsprüfung,
- Transaktionen,
- Retry-Logik,
- Registrierung in Frameworks.
Beispiel:
@misst_laufzeit
def lade_spielstand():
...
Weniger sinnvoll ist ein Decorator, wenn das Verhalten nur an einer Stelle gebraucht wird oder der Ablauf dadurch schwerer lesbar wird.
Statt:
@irgendwie_vorbereiten
def speichern():
...
ist manchmal klarer:
def speichern():
bereite_vor()
...
Decorators verstecken bewusst einen Teil des Ablaufs. Das ist nützlich, wenn der versteckte Teil allgemein und wiederverwendbar ist. Es ist schlecht, wenn dadurch fachliche Logik unsichtbar wird.
Decorators typisieren
Decorators mit Type Hints sauber zu typisieren, ist ein fortgeschritteneres Thema.
Für einfache Fälle kannst Du zunächst untypisiert bleiben.
Wenn Du die Signatur der dekorierten Funktion erhalten möchtest, helfen
ParamSpec und TypeVar:
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def mit_protokoll(funktion: Callable[P, R]) -> Callable[P, R]:
@wraps(funktion)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"{funktion.__name__} startet")
return funktion(*args, **kwargs)
return wrapper
Das bedeutet grob:
Psteht für die Parameter der ursprünglichen Funktion.Rsteht für ihren Rückgabetyp.- Der Decorator gibt wieder eine Funktion mit denselben Parametern und demselben Rückgabetyp zurück.
Für den Einstieg musst Du das nicht auswendig können. Wichtig ist nur: Decorators können typisiert werden, aber es ist anspruchsvoller als bei normalen Funktionen.
Testen von Decorators
Einen Decorator testest Du am einfachsten mit einer kleinen Testfunktion.
Beispiel für mit_protokoll(...):
def test_mit_protokoll_gibt_ergebnis_zurueck(capsys):
@mit_protokoll
def addiere(a, b):
return a + b
ergebnis = addiere(2, 3)
ausgabe = capsys.readouterr()
assert ergebnis == 5
assert "addiere startet" in ausgabe.out
assert "addiere fertig" in ausgabe.out
Für wraps kannst Du prüfen:
def test_mit_protokoll_erhaelt_name():
@mit_protokoll
def addiere(a, b):
"""Addiert zwei Zahlen."""
return a + b
assert addiere.__name__ == "addiere"
assert addiere.__doc__ == "Addiert zwei Zahlen."
Wenn ein Decorator mit State arbeitet, sollten Tests besonders darauf achten, dass sich Aufrufe nicht unerwartet gegenseitig beeinflussen.
Wann Decorators schwer werden
Decorators werden schwieriger, wenn sie:
- Argumente haben,
- Methoden und Funktionen gleichzeitig unterstützen sollen,
- asynchrone Funktionen unterstützen sollen,
- Generatorfunktionen unterstützen sollen,
- Descriptor-Verhalten verändern,
- Signaturen sichtbar erhalten sollen,
- komplexen State verwalten,
- mit anderen Decorators kombiniert werden.
Ein einfacher Logging-Decorator ist schnell geschrieben.
Ein Decorator, der perfekt mit normalen Funktionen, Methoden, async def,
Generatoren, Type Hints und Introspection harmoniert, ist deutlich anspruchsvoller.
Das ist normal.
Fange klein an und verwende für Standardfälle lieber bewährte Decorators aus der Standardbibliothek oder aus etablierten Frameworks.
Stolperfallen
-
@wrapsvergessen: Ohnefunctools.wrapsheißen dekorierte Funktionen oft nur nochwrapper, und Docstrings sowie andere Metadaten gehen verloren. -
Rückgabewert verlieren: Ein Decorator sollte den Rückgabewert der ursprünglichen Funktion zurückgeben, wenn er ihn nicht bewusst verändern soll.
-
Exceptions verschlucken: Fehler im Decorator nicht still in
NoneoderFalseverwandeln, wenn das nicht ausdrücklich gewollt ist. -
*argsund**kwargsvergessen: Dann funktioniert der Decorator nur für Funktionen mit genau der Signatur des Wrappers. -
State im Decorator unterschätzen: Veränderlicher Closure-State bleibt über Aufrufe hinweg erhalten.
-
Reihenfolge mehrerer Decorators verwechseln: Decorators werden von unten nach oben angewendet.
-
Decorator mit Argumenten falsch verschachteln:
@wiederhole(3)braucht eine Funktion, die einen Decorator zurückgibt. -
Zu viel fachliche Logik verstecken: Decorators eignen sich gut für Querschnittsverhalten, aber schlecht für überraschende Geschäftslogik.
-
@propertyfür teure Aktionen verwenden: Eine Property sollte sich wie ein Attribut anfühlen, nicht wie ein versteckter Datenbank- oder Dateizugriff. -
@staticmethodverwenden, obwohl eine Modulfunktion klarer wäre: Nicht jede Hilfsfunktion muss in eine Klasse. -
Caching ohne Grenzen einsetzen:
@cacheist praktisch, kann in langlebigen Programmen aber wachsen, wenn ständig neue Argumente auftauchen. -
Mutable oder unhashable Argumente cachen wollen:
functools.cacheundlru_cachebrauchen hashbare Argumente. -
Decorators als Magie behandeln:
@decoratorist eine Funktions- oder Klassenumwandlung. Wenn es unklar wird, schreibe die Langform gedanklich aus.
Kompakte Übersicht
| Ziel | Werkzeug |
|---|---|
| Funktion einwickeln | Decorator-Funktion |
| beliebige Argumente weiterreichen | *args, **kwargs |
| Rückgabewert erhalten | return funktion(...) |
| Metadaten erhalten | @functools.wraps(funktion) |
| Decorator anwenden | @decorator |
| Langform | funktion = decorator(funktion) |
| Decorator konfigurieren | Decorator Factory, z. B. @wiederhole(3) |
| Ergebnisse cachen | @functools.cache, @functools.lru_cache |
| berechnetes Attribut | @property |
| alternative Konstruktoren | @classmethod |
| Hilfsfunktion im Klassennamensraum | @staticmethod |
| Klassen automatisch ergänzen | @dataclass |
| mehrere Decorators | Anwendung von unten nach oben |
| Decorator typisieren | ParamSpec, TypeVar, Callable |
Übungen
1. Einen einfachen Decorator schreiben
Schreibe einen Decorator mit_log, der vor dem Funktionsaufruf den Namen der
Funktion ausgibt.
Lösung
from functools import wraps
def mit_log(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print(f"{funktion.__name__} wird aufgerufen")
return funktion(*args, **kwargs)
return wrapper
@mit_log
def begruesse(name):
print(f"Hallo, {name}!")
begruesse("Karl")
begruesse wird aufgerufen
Hallo, Karl!
2. Rückgabewert erhalten
Schreibe einen Decorator mit_log, der den Rückgabewert nicht verliert. Teste ihn
mit einer Funktion addiere(a, b).
Lösung
from functools import wraps
def mit_log(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print(f"{funktion.__name__} startet")
ergebnis = funktion(*args, **kwargs)
print(f"{funktion.__name__} fertig")
return ergebnis
return wrapper
@mit_log
def addiere(a, b):
return a + b
print(addiere(2, 3))
addiere startet
addiere fertig
5
3. Einen Decorator mit Argument schreiben
Schreibe @wiederhole(3), sodass eine Funktion dreimal ausgeführt wird.
Lösung
from functools import wraps
def wiederhole(mal):
def decorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
ergebnis = None
for _ in range(mal):
ergebnis = funktion(*args, **kwargs)
return ergebnis
return wrapper
return decorator
@wiederhole(3)
def klopf():
print("klopf")
klopf()
klopf
klopf
klopf
4. Decorator-Reihenfolge beobachten
Erzeuge zwei Decorators a und b, die jeweils vor und nach dem Aufruf etwas
ausgeben. Dekoriere eine Funktion mit beiden.
Lösung
from functools import wraps
def a(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print("a davor")
ergebnis = funktion(*args, **kwargs)
print("a danach")
return ergebnis
return wrapper
def b(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
print("b davor")
ergebnis = funktion(*args, **kwargs)
print("b danach")
return ergebnis
return wrapper
@a
@b
def hallo():
print("hallo")
hallo()
a davor
b davor
hallo
b danach
a danach
5. functools.cache verwenden
Schreibe eine rekursive Fibonacci-Funktion mit @cache.
Lösung
from functools import cache
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(40))
6. Eine Property bauen
Schreibe eine Klasse Spieler mit hp und einer Property ist_besiegt.
Lösung
class Spieler:
def __init__(self, hp):
self.hp = hp
@property
def ist_besiegt(self):
return self.hp <= 0
spieler = Spieler(0)
print(spieler.ist_besiegt)
True
Vertiefung
- Python lernen, Teil 11: Werkzeugkasten und Ausblick
- Generatoren und yield wirklich verstehen
- Type Hints in der Praxis
- Click: Kommandozeilen-Tools mit Decorators bauen
- Dataclasses: Klassen ohne Boilerplate
- Logging statt print
Fazit
Decorators sind weniger geheimnisvoll, als sie aussehen.
Diese Schreibweise:
@decorator
def funktion():
...
bedeutet im Kern:
def funktion():
...
funktion = decorator(funktion)
Ein Decorator nimmt also eine Funktion entgegen und gibt eine neue oder veränderte Funktion zurück.
Mit *args, **kwargs und functools.wraps werden Decorators alltagstauglich.
Mit einer zusätzlichen Ebene entstehen Decorators mit Argumenten wie
@wiederhole(3). Und mit Standard-Decorators wie @property, @classmethod,
@staticmethod, @dataclass, @cache und @lru_cache nutzt Du dieses Prinzip
ständig, auch wenn Du keine eigenen Decorators schreibst.
Der wichtigste Gedanke:
Decorators sind gut für wiederverwendbares Verhalten rund um Funktionen oder Klassen. Sie sollten Code klarer machen, nicht verstecken.
Wenn Dir ein Decorator magisch vorkommt, schreibe ihn gedanklich in die Langform um. Dann bleibt meistens nur normale Python-Mechanik übrig.
Weiterlesen
- Python-Dokumentation:
functools - Python-Dokumentation:
property - Python-Dokumentation:
staticmethod - Python-Dokumentation:
classmethod - Python-Dokumentation:
dataclasses - Python-Dokumentation: Compound statements – Function definitions
- PEP 318: Decorators for Functions and Methods
- PEP 3129: Class Decorators
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.