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.
Zum Beispiel:
@mit_protokoll
def speichern(name):
...
Oder:
@dataclass
class Gegenstand:
...
Oder bei Click:
@click.command()
def status():
...
Die Syntax wirkt zunächst nach einer besonderen Python-Magie. Dahinter steckt aber ein ziemlich normales Prinzip:
Ein Decorator bekommt ein Objekt und sein Rückgabewert tritt an dessen Stelle.
Bei Funktionen kannst Du Dir:
@decorator
def funktion():
...
zunächst ungefähr so vorstellen:
def funktion():
...
funktion = decorator(funktion)
Wenn dieser Zusammenhang sitzt, wird der Rest deutlich weniger mysteriös.
Funktionen sind Objekte
Eine Funktion ist in Python selbst ein Objekt.
def begruesse():
return "Hallo"
Der Name:
begruesse
zeigt auf dieses Funktionsobjekt.
Die Klammern:
begruesse()
rufen es dagegen auf.
Das lässt sich direkt sehen:
print(begruesse)
print(begruesse())
Ausgabe ungefähr:
<function begruesse at 0x...>
Hallo
Weil Funktionen normale Objekte sind, kannst Du sie auch einer anderen Variablen zuweisen:
funktion = begruesse
print(
funktion()
)
Ausgabe:
Hallo
funktion und begruesse zeigen hier auf dasselbe Funktionsobjekt.
Diese Eigenschaft ist die Grundlage von Decorators.
Funktionen weiterreichen
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, dass es speziell begruesse(...) bekommen hat. Es
bekommt einfach ein aufrufbares Objekt und verwendet es später.
Das kennst Du längst aus der Standardbibliothek:
namen = [
"Karl",
"Alexandra",
"Mia",
]
sortiert = sorted(
namen,
key=len,
)
print(sortiert)
Ausgabe:
['Mia', 'Karl', 'Alexandra']
Bei:
key=len
wird len nicht aufgerufen.
Wir übergeben die Funktion selbst. sorted(...) ruft sie später für die
einzelnen Werte auf.
Funktionen können Funktionen zurückgeben
Auch die andere Richtung funktioniert:
def baue_begruessung():
def begruesse(name):
return f"Hallo, {name}!"
return begruesse
Aufruf:
funktion = baue_begruessung()
print(
funktion("Karl")
)
Ausgabe:
Hallo, Karl!
baue_begruessung() erzeugt eine innere Funktion und gibt sie zurück.
Damit haben wir bereits fast alles, was für einen einfachen Decorator nötig ist:
Funktion hinein
↓
andere Funktion erzeugen
↓
Funktion zurückgeben
Eine Funktion einwickeln
Bauen wir einen ersten Decorator:
def mit_protokoll(funktion):
def wrapper():
print(
f"[log] {funktion.__name__} startet"
)
ergebnis = funktion()
print(
f"[log] {funktion.__name__} fertig"
)
return ergebnis
return wrapper
Unsere eigentliche Funktion:
def speichern():
print(
"Spielstand gespeichert."
)
Nun 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 auf den wrapper.
Dieser ruft intern weiterhin die ursprüngliche Funktion auf.
Der Ablauf sieht also so aus:
speichern()
↓
wrapper()
↓
Text davor
↓
ursprüngliches speichern()
↓
Text danach
Das ist das klassische Wrapper-Muster hinter vielen Function Decorators.
@decorator ist die Kurzform
Statt:
def speichern():
print(
"Spielstand gespeichert."
)
speichern = mit_protokoll(
speichern
)
schreiben wir:
@mit_protokoll
def speichern():
print(
"Spielstand gespeichert."
)
Die Wirkung ist dieselbe.
Wichtig ist nur der Zeitpunkt: Der Decorator wird nicht erst ausgeführt, wenn Du später:
speichern()
aufrufst.
Die Decoration passiert bereits, wenn Python die Funktionsdefinition ausführt.
Bei Funktionen auf Modulebene ist das normalerweise während des Imports des Moduls.
Decorators laufen schon beim Definieren
Das lässt sich leicht beobachten:
def registriere(funktion):
print(
f"registriere {funktion.__name__}"
)
return funktion
@registriere
def speichern():
print(
"speichern läuft"
)
print(
"Modul ist fertig definiert"
)
Ausgabe:
registriere speichern
Modul ist fertig definiert
Wir haben:
speichern()
noch gar nicht aufgerufen.
Das:
registriere(speichern)
passiert bereits bei der Definition.
Erst ein späteres:
speichern()
führt den Body von speichern(...) aus.
Das ist bei Frameworks wichtig. Ein Decorator kann beispielsweise schon beim Import eine Route, einen CLI-Command oder einen Handler registrieren.
Deshalb sollte ein Decorator bei der Definition nicht unbedacht teure oder überraschende Arbeit ausführen.
Ein Decorator muss keine neue Funktion zurückgeben
Die vereinfachte Erklärung:
Ein Decorator nimmt eine Funktion und gibt eine neue Funktion zurück.
ist für Wrapper gut, aber technisch zu eng.
Ein Decorator muss zunächst aufrufbar sein. Sein Rückgabewert wird anschließend an den dekorierten Namen gebunden.
Er kann beispielsweise die ursprüngliche Funktion unverändert zurückgeben:
REGISTRY = {}
def registriere(funktion):
REGISTRY[
funktion.__name__
] = funktion
return funktion
Dann:
@registriere
def speichern():
print(
"Spielstand gespeichert."
)
Der Decorator hat hier hauptsächlich einen Seiteneffekt:
print(REGISTRY)
enthält anschließend einen Eintrag für speichern.
Andere Decorators geben wiederum ganz andere Objekte zurück.
@property erzeugt beispielsweise einen Descriptor. @click.command() macht
aus einer Funktion ein Click-Command-Objekt.
Das erklärt auch, warum ein dekorierter Name sich manchmal anders verhält als die ursprüngliche Funktion.
Argumente mit *args und **kwargs weiterreichen
Unser erster Wrapper funktioniert nur für Funktionen ohne Parameter:
def wrapper():
...
Diese Funktion:
def speichern(name):
print(
f"{name} gespeichert."
)
könnte er nicht korrekt aufrufen.
Ein allgemeiner Wrapper nimmt deshalb beliebige Argumente entgegen:
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
Nun:
@mit_protokoll
def speichern(
name,
datei="spielstand.json",
):
print(
f"{name} -> {datei}"
)
speichern(
"Karl",
datei="save.json",
)
Der Wrapper sammelt:
positionale Argumente -> args
Keyword Arguments -> kwargs
und reicht beide wieder an die ursprüngliche Funktion weiter.
Den Rückgabewert nicht verlieren
Ein häufiger Decorator-Bug sieht so aus:
def mit_protokoll(funktion):
def wrapper(
*args,
**kwargs,
):
print("Starte")
funktion(
*args,
**kwargs,
)
print("Fertig")
return wrapper
Dekorieren wir damit:
@mit_protokoll
def addiere(
a,
b,
):
return a + b
dann:
ergebnis = addiere(
2,
3,
)
print(ergebnis)
Ausgabe:
Starte
Fertig
None
Der ursprüngliche Rückgabewert wurde zwar erzeugt, aber vom Wrapper verworfen.
Richtig:
def mit_protokoll(funktion):
def wrapper(
*args,
**kwargs,
):
print("Starte")
ergebnis = funktion(
*args,
**kwargs,
)
print("Fertig")
return ergebnis
return wrapper
Nun bleibt das Verhalten der ursprünglichen Funktion erhalten.
Als brauchbare Default-Regel:
Wenn Dein Decorator den Rückgabewert nicht absichtlich verändert, reiche ihn unverändert weiter.
Closures: wie der Wrapper die ursprüngliche Funktion kennt
Im Wrapper verwenden wir:
funktion
obwohl mit_protokoll(...) längst beendet ist:
def mit_protokoll(funktion):
def wrapper(
*args,
**kwargs,
):
return funktion(
*args,
**kwargs,
)
return wrapper
Das funktioniert wegen einer Closure.
Die innere Funktion behält Zugriff auf Namen aus ihrem umgebenden Scope.
Ein kleineres Beispiel:
def multiplizierer(
faktor,
):
def rechne(zahl):
return zahl * faktor
return rechne
Nun:
verdopple = multiplizierer(2)
verdreifache = multiplizierer(3)
print(
verdopple(10)
)
print(
verdreifache(10)
)
Ausgabe:
20
30
Beide erzeugten Funktionen besitzen ihre eigene Closure.
Die eine erinnert sich an:
faktor == 2
die andere an:
faktor == 3
Beim Decorator merkt sich der Wrapper auf dieselbe Weise die ursprüngliche Funktion.
functools.wraps gehört in Wrapper-Decorators
Unser Decorator hat noch ein Problem:
def mit_protokoll(funktion):
def wrapper(
*args,
**kwargs,
):
return funktion(
*args,
**kwargs,
)
return wrapper
Dekorieren wir:
@mit_protokoll
def speichern(name):
"""Speichert den Spielstand."""
print(
f"{name} gespeichert."
)
und prüfen:
print(
speichern.__name__
)
print(
speichern.__doc__
)
bekommen wir:
wrapper
None
Von außen sieht die dekorierte Funktion nun wie der Wrapper aus.
Das ist schlecht für:
Dokumentation
Debugging
Introspection
Frameworks
Editor-Unterstützung
Dafür gibt es:
functools.wraps
from functools import wraps
def mit_protokoll(funktion):
@wraps(funktion)
def wrapper(
*args,
**kwargs,
):
return funktion(
*args,
**kwargs,
)
return wrapper
Nun:
@mit_protokoll
def speichern(name):
"""Speichert den Spielstand."""
print(
f"{name} gespeichert."
)
ergibt:
print(
speichern.__name__
)
print(
speichern.__doc__
)
speichern
Speichert den Spielstand.
Was wraps tatsächlich erhält
@wraps(funktion) kopiert beziehungsweise aktualisiert mehrere Metadaten des
Wrappers.
Dazu gehören unter aktuellen Python-Versionen (ab 3.14 __annotate__ statt
__annotations__) unter anderem:
__module__
__name__
__qualname__
__annotations__
__type_params__
__doc__
Außerdem bekommt der Wrapper:
__wrapped__
mit einem Verweis auf die eingewickelte Funktion.
Das ist für Introspection nützlich:
print(
speichern.__wrapped__
)
Werkzeuge wie inspect.signature(...) folgen diesem __wrapped__-Verweis
standardmäßig ebenfalls.
wraps macht aus:
def wrapper(*args, **kwargs):
...
aber nicht magisch dieselbe Python-Funktion wie das Original. Der Wrapper bleibt eine eigene Funktion mit eigenem Code.
Auch die statische Typisierung musst Du separat korrekt beschreiben.
Der übliche Function Decorator
Damit ergibt sich für einen einfachen Wrapper dieses Grundmuster:
from functools import wraps
def decorator(funktion):
@wraps(funktion)
def wrapper(
*args,
**kwargs,
):
# Verhalten davor
ergebnis = funktion(
*args,
**kwargs,
)
# Verhalten danach
return ergebnis
return wrapper
Nicht jeder Decorator braucht exakt diese Form.
Wenn Du aber eine normale synchrone Funktion lediglich einwickeln willst, ist sie ein guter Ausgangspunkt.
Ein Logging-Decorator
Mit echtem Logging:
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__,
)
try:
return funktion(
*args,
**kwargs,
)
finally:
logger.info(
"%s fertig",
funktion.__name__,
)
return wrapper
Verwendung:
@mit_protokoll
def speichern(name):
print(
f"{name} gespeichert."
)
Durch finally wird die Abschlussmeldung auch dann geschrieben, wenn die
Funktion mit einer Exception endet.
Ob Du das möchtest, hängt natürlich von der Bedeutung der Meldung ab. Ein Text wie:
speichern erfolgreich
wäre in finally falsch.
speichern beendet
passt dagegen.
Exceptions protokollieren und weiterwerfen
Ein Decorator kann Fehler zentral 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
Entscheidend ist:
raise
Die ursprüngliche Exception wird nach dem Logging weitergegeben.
Ein allgemeiner Decorator wie:
except Exception:
logger.exception(
"Fehler"
)
return None
würde dagegen die Semantik jeder dekorierten Funktion verändern.
Das kann absichtlich sinnvoll sein, sollte aber niemals aus Versehen passieren.
Laufzeit messen
Ein weiterer typischer Wrapper:
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:
dauer = (
perf_counter()
- start
)
print(
f"{funktion.__name__} "
f"dauerte {dauer:.3f}s"
)
return wrapper
Verwendung:
@misst_laufzeit
def lade_raeume():
return [
"halle",
"bibliothek",
"krypta",
]
perf_counter() ist für das Messen einer verstrichenen Dauer gedacht.
Für ernsthafte Performance-Analysen ersetzt so ein Decorator natürlich keinen Profiler. Für gezielte Messungen kann er trotzdem nützlich sein.
Decorators mit eigenen Argumenten
Bisher sah unser Decorator so aus:
@mit_protokoll
def speichern():
...
Manchmal soll er selbst konfiguriert werden:
@wiederhole(3)
def klopf():
print("klopf")
Nun passiert bei der Definition zuerst:
wiederhole(3)
Das Ergebnis davon muss der eigentliche Decorator sein.
Wir brauchen deshalb eine weitere Ebene:
from functools import wraps
def wiederhole(mal):
if mal < 1:
raise ValueError(
"mal muss mindestens 1 sein"
)
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
Nun:
@wiederhole(3)
def klopf():
print("klopf")
klopf()
Ausgabe:
klopf
klopf
klopf
Die Langform einer Decorator Factory
Diese Syntax:
@wiederhole(3)
def klopf():
...
kannst Du gedanklich zerlegen:
def klopf():
...
Dann:
decorator = wiederhole(3)
und schließlich:
klopf = decorator(
klopf
)
Zusammen:
klopf = wiederhole(3)(
klopf
)
Damit sind die drei Funktionsebenen leichter auseinanderzuhalten:
wiederhole(3)
↓
decorator(funktion)
↓
wrapper(*args, **kwargs)
Der Begriff Decorator Factory ist dafür hilfreich:
Eine Decorator Factory ist eine Funktion, die einen Decorator erzeugt.
Retry als Decorator
Ein etwas realistischeres Beispiel:
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
assert (
letzter_fehler
is not None
)
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 wiederholt bewusst nur bei:
OSError
und nicht pauschal bei jedem Exception.
Eine TypeError wegen eines Programmierfehlers dreimal hintereinander
auszuführen würde das Problem schließlich nicht lösen.
In echten Netzwerk- oder API-Clients gehören zu Retry-Logik oft noch Backoff, Jitter und eine klare Entscheidung darüber, welche Fehler überhaupt wiederholbar sind. Das Grundmuster bleibt aber dasselbe.
Mehrere Decorators
Python erlaubt mehrere Decorators:
@decorator_a
@decorator_b
def funktion():
...
Die Anwendung ist verschachtelt.
Gedanklich:
funktion = decorator_a(
decorator_b(
funktion
)
)
Der Decorator direkt über der Funktion bekommt sie also zuerst.
Ein sichtbares 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 wickelt zuerst hallo ein.
Danach wickelt a das Ergebnis von b ein.
Das ist relevant bei Decorators für:
Authentifizierung
Caching
Transaktionen
Retries
Logging
Framework-Registrierung
Die Reihenfolge kann das Verhalten verändern.
Decorator-Ausdrücke werden bei der Definition ausgewertet
Auch bei:
@decorator_a(option())
@decorator_b
def funktion():
...
passiert die Vorbereitung der Decorators während der Funktionsdefinition.
Gerade Decorator Factories sollten deshalb keine überraschenden Laufzeitoperationen durchführen, die eigentlich erst beim späteren Funktionsaufruf erwartet werden.
Ein:
@lade_konfiguration_aus_dem_netz()
def funktion():
...
könnte beispielsweise schon beim Import einen Netzwerkzugriff auslösen.
Technisch ist das möglich. Als API-Design ist es meistens keine gute Überraschung.
State in einer Closure
Closures können veränderlichen State behalten:
from functools import wraps
def zaehle_aufrufe(funktion):
anzahl = 0
@wraps(funktion)
def wrapper(
*args,
**kwargs,
):
nonlocal anzahl
anzahl += 1
print(
f"{funktion.__name__} "
f"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 zum erzeugten Wrapper.
Dekorierst Du eine zweite Funktion:
@zaehle_aufrufe
def speichern():
...
bekommt sie ihren eigenen Zähler.
Closure-State ist nicht automatisch threadsicher
Sobald mehrere Threads denselben dekorierten Callable gleichzeitig verwenden, wird mutable State interessanter.
Dieser Code:
anzahl += 1
sollte nicht einfach deshalb als sicher betrachtet werden, weil er innerhalb einer Closure steckt.
Wenn korrekte Synchronisation wichtig ist, brauchst Du dieselben Überlegungen wie bei anderem gemeinsam verändertem State.
Für Logging-Decorators ohne eigenen veränderlichen State spielt das meistens keine Rolle.
Für Counter, Rate Limiter oder eigene Caches dagegen schon.
Ein Wrapper passt nicht automatisch zu async def
Ein Decorator für normale Funktionen:
def misst_laufzeit(funktion):
@wraps(funktion)
def wrapper(
*args,
**kwargs,
):
start = perf_counter()
try:
return funktion(
*args,
**kwargs,
)
finally:
...
funktioniert syntaktisch auch auf:
async def lade_daten():
...
misst dann aber nicht das, was Du vermutlich erwartest.
Der Aufruf einer Coroutine Function führt ihren eigentlichen Body noch nicht aus. Er erzeugt zunächst ein Coroutine-Objekt.
Der synchrone Wrapper wäre deshalb schon „fertig“, bevor das await der
eigentlichen Operation stattgefunden hat.
Für Async-Code braucht der Wrapper selbst async def:
from functools import wraps
from time import perf_counter
def misst_laufzeit_async(
funktion,
):
@wraps(funktion)
async def wrapper(
*args,
**kwargs,
):
start = perf_counter()
try:
return await funktion(
*args,
**kwargs,
)
finally:
dauer = (
perf_counter()
- start
)
print(
f"{funktion.__name__} "
f"dauerte {dauer:.3f}s"
)
return wrapper
Nun umfasst die Messung tatsächlich die asynchrone Ausführung.
Dasselbe Problem gibt es bei Generatorfunktionen
Eine Generatorfunktion:
def zahlen():
yield 1
yield 2
führt ihren Body beim Aufruf ebenfalls noch nicht aus.
Sie erzeugt zunächst einen Generator.
Ein normaler Logging-Wrapper könnte deshalb:
startet
fertig
loggen, obwohl noch kein einziger Wert des Generators verarbeitet wurde.
Soll der Decorator die tatsächliche Iteration umschließen, muss auch der Wrapper ein Generator sein:
from functools import wraps
def logge_generator(funktion):
@wraps(funktion)
def wrapper(
*args,
**kwargs,
):
print(
f"{funktion.__name__} startet"
)
try:
return (
yield from funktion(
*args,
**kwargs,
)
)
finally:
print(
f"{funktion.__name__} fertig"
)
return wrapper
Mehr zu diesem Verhalten findest Du in Generatoren und yield wirklich verstehen.
Das zeigt eine wichtige Grenze der simplen Formel:
def wrapper(*args, **kwargs):
return funktion(
*args,
**kwargs,
)
Sie ist universell aufrufbar, erhält aber nicht automatisch die Semantik jeder Art von Callable.
Decorators für Methoden
Ein normaler Function Decorator mit einem Function Wrapper funktioniert auch für Methoden:
from functools import wraps
def mit_log(funktion):
@wraps(funktion)
def wrapper(
*args,
**kwargs,
):
print(
funktion.__name__
)
return funktion(
*args,
**kwargs,
)
return wrapper
Dann:
class Spieler:
@mit_log
def heile(
self,
menge,
):
self.hp += menge
self steckt einfach im ersten positional Argument des Wrappers.
Interessanter wird es, wenn ein Decorator statt einer normalen Funktion ein eigenes Callable-Objekt zurückgibt. Dann spielt Pythons Descriptor-Protokoll eine Rolle, damit Methoden weiterhin korrekt an Instanzen gebunden werden.
Für normale Wrapper-Funktionen musst Du Dich damit zunächst nicht beschäftigen.
@property: ein Decorator, der keine Funktion zurückgibt
Ein bekanntes Beispiel aus Python selbst:
class Spieler:
def __init__(
self,
name,
hp,
):
self.name = name
self.hp = hp
@property
def ist_besiegt(self):
return self.hp <= 0
Verwendung:
spieler = Spieler(
"Karl",
hp=0,
)
print(
spieler.ist_besiegt
)
Ausgabe:
True
Ohne @property wäre:
spieler.ist_besiegt()
nötig.
property(...) nimmt die Funktion entgegen und erzeugt ein Property-Objekt.
Dieses Objekt implementiert das Descriptor-Protokoll und steuert den späteren
Attributzugriff.
Das ist ein gutes Beispiel dafür, warum ein Decorator nicht zwangsläufig eine Wrapper-Funktion zurückgibt.
Properties sollten wie Attribute wirken
Eine Property passt gut für eine berechnete Eigenschaft:
@property
def ist_besiegt(self):
return self.hp <= 0
Weniger passend:
@property
def spielstand(self):
return lade_grosse_datei_aus_dem_netz()
Ein Zugriff wie:
spieler.spielstand
sieht billig und nebenwirkungsarm aus.
Wenn dahinter plötzlich ein langsamer Netzwerkrequest steckt, überrascht das den Leser.
Eine Property sollte sich nach Möglichkeit wie ein normales Attribut anfühlen.
Property Setter
Properties können einen Setter besitzen:
class Spieler:
def __init__(
self,
hp=100,
):
self._hp = 0
self.hp = hp
@property
def hp(self):
return self._hp
@hp.setter
def hp(
self,
wert,
):
self._hp = max(
wert,
0,
)
Nun:
spieler = Spieler()
spieler.hp = -50
print(
spieler.hp
)
Ausgabe:
0
Für reine Invarianten kann das sinnvoll sein.
Fachliche Aktionen sind als Methoden oft klarer:
spieler.erleide_schaden(
50
)
transportiert mehr Bedeutung als eine überraschend komplexe Zuweisung an
spieler.hp.
cached_property
Für teuer berechnete Eigenschaften gibt es außerdem:
functools.cached_property
from functools import cached_property
class Raum:
def __init__(
self,
daten,
):
self.daten = daten
@cached_property
def statistik(self):
print(
"berechne Statistik"
)
return berechne_statistik(
self.daten
)
Beim ersten:
raum.statistik
wird der Wert berechnet und anschließend im Objekt gespeichert.
Weitere Zugriffe verwenden den gespeicherten Wert.
Löschen kannst Du ihn mit:
del raum.statistik
Dann wird er beim nächsten Zugriff neu berechnet.
cached_property benötigt normalerweise ein beschreibbares __dict__ der
Instanz. Eine reine __slots__-Klasse ohne __dict__ kann es deshalb nicht
einfach verwenden.
Außerdem garantiert cached_property nicht, dass der Getter bei parallelem
Zugriff mehrerer Threads exakt einmal ausgeführt wird.
Wenn genau das notwendig ist, brauchst Du zusätzliche Synchronisation.
@staticmethod
Auch staticmethod ist ein Decorator:
class Wuerfel:
@staticmethod
def ist_gueltige_seitenzahl(
seiten,
):
return seiten >= 2
Aufruf:
print(
Wuerfel.ist_gueltige_seitenzahl(
6
)
)
Ausgabe:
True
Eine Static Method bekommt beim Zugriff weder:
self
noch:
cls
automatisch.
Sie liegt lediglich logisch im Namensraum der Klasse.
Das ist manchmal passend. Wenn die Funktion mit der Klasse kaum etwas zu tun hat, kann eine normale Modulfunktion trotzdem klarer sein.
@classmethod
Eine Class Method bekommt die konkrete Klasse als ersten Parameter:
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
)
@classmethod ist besonders praktisch für alternative Konstruktoren.
Die Verwendung von:
cls(...)
statt:
Spieler(...)
hat außerdem den Vorteil, dass Unterklassen grundsätzlich die passende Klasse weiterreichen können.
@dataclass: Decorators funktionieren auch auf Klassen
Nicht nur Funktionen lassen sich dekorieren.
from dataclasses import dataclass
@dataclass
class Gegenstand:
name: str
beschreibung: str = ""
Gedanklich entspricht die Syntax wieder ungefähr:
class Gegenstand:
name: str
beschreibung: str = ""
Gegenstand = dataclass(
Gegenstand
)
dataclass(...) untersucht die Klasse und erzeugt abhängig von der
Konfiguration unter anderem Methoden wie:
__init__
__repr__
__eq__
Normalerweise arbeitet dataclass mit der übergebenen Klasse.
Eine interessante Ausnahme ist:
@dataclass(
slots=True,
)
Dabei wird eine neue Klasse zurückgegeben, weil die Slots nicht einfach nachträglich in die bereits erzeugte Klasse eingebaut werden können.
Mehr dazu findest Du in Dataclasses: Klassen ohne Boilerplate.
Framework-Decorators tun oft mehr als Wrapping
Bei einem Webframework kann ein Decorator so aussehen:
@app.get("/status")
def status():
return {
"ok": True,
}
Bei Click:
@click.command()
@click.option(
"--name",
)
def begruesse(name):
click.echo(
f"Hallo, {name}"
)
Bei pytest:
@pytest.fixture
def spieler():
return Spieler(
"Test"
)
Hier sollte man nicht automatisch annehmen:
Decorator = vor Funktion etwas tun, danach Funktion aufrufen.
Frameworks können Decorators verwenden, um:
Funktionen zu registrieren
Metadaten zu speichern
andere Objekte zu erzeugen
Descriptor-Verhalten einzubauen
Callbacks in einen Command- oder Route-Baum einzuhängen
Das gemeinsame Sprachmerkmal bleibt:
Der Decorator erhält das gerade definierte Objekt und sein Ergebnis wird an dessen Namen gebunden.
Was der Decorator intern daraus macht, entscheidet seine Implementierung.
Caching mit functools.cache
Ein sehr praktischer Standard-Decorator 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 die naive rekursive Variante dieselben Teilprobleme immer wieder.
Mit:
print(
fibonacci(40)
)
werden bereits berechnete Ergebnisse wiederverwendet.
@cache ist dabei die unbeschränkte Variante eines Memoization Cache.
Cache-Argumente müssen hashbar sein
Die Argumente bilden die Cache Keys.
Darum funktioniert:
fibonacci(40)
problemlos.
Eine Liste als Argument dagegen nicht:
from functools import cache
@cache
def summe(werte):
return sum(werte)
summe(
[
1,
2,
3,
]
)
Listen sind nicht hashbar.
Ein Tuple wäre geeignet:
summe(
(
1,
2,
3,
)
)
Das gilt auch für lru_cache.
lru_cache begrenzt den Cache
Mit:
from functools import lru_cache
@lru_cache(
maxsize=128,
)
def berechne(zahl):
return zahl * zahl
werden höchstens die 128 zuletzt verwendeten Cache-Einträge gehalten.
Mit:
maxsize=None
ist der Cache unbeschränkt.
@cache ist die einfache optimierte Form dieses unbeschränkten Falls.
Für langlebige Programme ist das ein wichtiger Unterschied.
Ein Cache, dessen mögliche Keys ständig wachsen:
Benutzer-ID
Request-ID
Timestamp
zufälliger Token
kann mit @cache immer mehr Speicher halten.
Caches halten Objekte fest
Ein Cache speichert nicht nur das Ergebnis abstrakt irgendwo.
Er hält Referenzen auf die verwendeten Argumente und Rückgabewerte, solange der Eintrag im Cache bleibt.
Auch deshalb sollte ein unbeschränkter Cache bewusst eingesetzt werden.
Bei Bedarf:
funktion.cache_clear()
löscht die gespeicherten Ergebnisse.
Statistiken bekommst Du mit:
funktion.cache_info()
zum Beispiel über Hits und Misses.
Threadsafe heißt nicht „wird garantiert einmal berechnet“
cache und lru_cache schützen ihre internen Datenstrukturen auch bei
parallelem Zugriff mehrerer Threads.
Trotzdem kann die eingewickelte Funktion für denselben Key mehr als einmal gleichzeitig ausgeführt werden, wenn mehrere Threads den noch nicht vorhandenen Wert nahezu gleichzeitig anfordern.
Der Cache bleibt dabei konsistent.
Die Funktion selbst bekommt aber keine Exactly-once-Garantie.
Das ist relevant, wenn die gecachte Funktion Nebenwirkungen hat.
Ohnehin sind Funktionen mit Nebenwirkungen meist schlechte Kandidaten für Memoization.
Decorator-Reihenfolge bei Standard-Decorators
Auch Standard-Decorators folgen der normalen Verschachtelung.
Bei Kombinationen kann die Reihenfolge deshalb Teil des API sein.
Ein Beispiel aus der Standardbibliothek ist singledispatchmethod, das bei
Kombinationen mit Decorators wie @classmethod außen liegen muss, damit sein:
.register
weiter verfügbar bleibt.
Du musst solche Sonderregeln nicht auswendig lernen.
Wenn ein Framework oder Standard-Decorator mit einem anderen kombiniert wird, lohnt sich aber immer ein Blick in dessen Dokumentation.
singledispatch: Dispatch per Typ
Ein weiterer interessanter Standard-Decorator ist:
functools.singledispatch
Er erlaubt unterschiedliche Implementierungen abhängig vom Typ des ersten Arguments:
from functools import singledispatch
@singledispatch
def beschreibe(wert):
return repr(wert)
@beschreibe.register
def _(wert: int):
return (
f"Zahl: {wert}"
)
@beschreibe.register
def _(wert: str):
return (
f"Text: {wert}"
)
Nun:
print(
beschreibe(42)
)
print(
beschreibe("Fackel")
)
Ausgabe:
Zahl: 42
Text: Fackel
Hier dient der Decorator nicht primär dazu, Code vor und nach einer Funktion auszuführen.
Er baut eine Generic Function mit einem Registry-basierten Dispatch.
Auch das gehört zum Decorator-Prinzip.
Decorators sauber typisieren
Bei einem untypisierten Wrapper:
def mit_protokoll(funktion):
def wrapper(
*args,
**kwargs,
):
return funktion(
*args,
**kwargs,
)
return wrapper
weiß ein statischer Type Checker nur wenig über den Zusammenhang zwischen ursprünglicher und zurückgegebener Funktion.
Für einen Decorator, der die Signatur erhält, sind ParamSpec und ein Type
Parameter für den Rückgabewert gedacht.
Mit der modernen Syntax ab Python 3.12:
from collections.abc import Callable
from functools import wraps
def mit_protokoll[
T,
**P,
](
funktion: Callable[P, T],
) -> Callable[P, T]:
@wraps(funktion)
def wrapper(
*args: P.args,
**kwargs: P.kwargs,
) -> T:
print(
f"{funktion.__name__} startet"
)
return funktion(
*args,
**kwargs,
)
return wrapper
P steht für die komplette Parameterliste.
T steht für den Rückgabewert.
Damit kann der Checker verstehen:
Decorator bekommt Callable mit Signatur P -> T
Decorator liefert wieder Callable P -> T
Ältere Syntax mit ParamSpec und TypeVar
Wenn Dein Projekt Python 3.11 oder älter unterstützt:
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
def mit_protokoll(
funktion: Callable[P, T],
) -> Callable[P, T]:
@wraps(funktion)
def wrapper(
*args: P.args,
**kwargs: P.kwargs,
) -> T:
return funktion(
*args,
**kwargs,
)
return wrapper
Das beschreibt denselben Zusammenhang.
Mehr zu Callable und Type Hints allgemein findest Du in
Type Hints in der Praxis; ParamSpec selbst
beschreibt PEP 612.
wraps und Type Hints lösen unterschiedliche Probleme
Die beiden Dinge werden gerne vermischt.
@wraps(funktion)
hilft zur Laufzeit bei Metadaten und Introspection.
Callable[P, T]
hilft dem statischen Type Checker.
Ein Decorator kann also @wraps korrekt verwenden und trotzdem schlecht
typisiert sein.
Oder statisch korrekt typisiert sein, während __name__ und __doc__
versehentlich auf den Wrapper zeigen.
Für einen guten allgemeinen Wrapper sind oft beide Ebenen sinnvoll.
Wenn ein Decorator die Signatur absichtlich verändert
Nicht jeder Decorator erhält dieselben Parameter.
Angenommen, ein Decorator fügt vor der ursprünglichen Signatur einen Context hinzu oder entfernt für den äußeren Aufrufer einen Parameter.
Dann reicht:
Callable[P, T]
->
Callable[P, T]
nicht mehr.
Für solche Fälle gibt es unter anderem:
typing.Concatenate
Das ist fortgeschritteneres Typing.
Wichtig für den Einstieg ist nur:
ParamSpecist nicht bloß dazu da,*argsund**kwargshübscher hinzuschreiben. Es beschreibt die Beziehung zwischen zwei Callable-Signaturen.
Decorators testen
Ein Decorator lässt sich mit kleinen Funktionen gut isoliert testen.
Für unseren print-basierten Wrapper mit_protokoll aus dem Abschnitt
„Argumente mit *args und **kwargs weiterreichen“:
def test_mit_protokoll(
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
)
Falls der Decorator auch eine Abschlussmeldung erzeugt:
assert (
"addiere fertig"
in ausgabe.out
)
Metadaten testen
Wenn wraps wichtig für das API ist:
def test_metadaten_bleiben_erhalten():
@mit_protokoll
def addiere(
a,
b,
):
"""Addiert zwei Zahlen."""
return a + b
assert (
addiere.__name__
== "addiere"
)
assert (
addiere.__doc__
== "Addiert zwei Zahlen."
)
Auch:
addiere.__wrapped__
kann interessant sein, wenn andere Tools auf die Originalfunktion zugreifen müssen.
Ausnahmen ebenfalls testen
Ein Decorator, der Exceptions nur protokollieren soll, darf sie nicht verschlucken:
import pytest
def test_fehler_wird_weitergegeben():
@logge_fehler
def kaputt():
raise ValueError(
"kaputt"
)
with pytest.raises(
ValueError,
match="kaputt",
):
kaputt()
Gerade bei Decorators lohnt sich dieser Test, weil Wrapper unbeabsichtigt leicht Rückgabewerte oder Exceptions verändern.
Decorator oder normale Funktion?
Decorators sind angenehm für Querschnittsverhalten.
Zum Beispiel:
Logging
Timing
Caching
Retry
Berechtigungen
Transaktionen
Registrierung
Tracing
Das Verhalten betrifft viele Funktionen auf dieselbe Art.
Dann ist:
@misst_laufzeit
def lade_spielstand():
...
oft klarer als dieselbe Messlogik in jede Funktion zu kopieren.
Weniger passend ist ein Decorator, wenn er wichtige fachliche Schritte versteckt.
Statt:
@bezahle_gold
def kaufe_schwert():
...
kann:
def kaufe_schwert(
spieler,
):
spieler.bezahle(
50
)
...
für einen Leser wesentlich klarer sein.
Decorators machen einen Teil des Kontrollflusses bewusst unsichtbar.
Das ist ihr Vorteil und gleichzeitig ihre Gefahr.
Wann ein eigener Decorator zu kompliziert wird
Ein einfacher Wrapper ist schnell geschrieben.
Schwieriger wird es, wenn derselbe Decorator gleichzeitig perfekt unterstützen soll:
normale Funktionen
Methoden
async def
Generatorfunktionen
eigene Descriptoren
Signaturveränderungen
statische Typisierung
mehrere verschachtelte Decorators
mutablem State
Threading
Dann baust Du schnell mehr Infrastruktur als eigentliche Anwendung.
Für Standardfälle ist deshalb ein vorhandener Decorator aus der Standardbibliothek oder einem etablierten Framework häufig die bessere Wahl.
Eigene Decorators sollten einen klaren, kleinen Vertrag haben.
Häufige Stolperfallen
-
@decoratorfür Aufrufsyntax halten: Der Decorator wird bei der Definition angewendet, nicht erst beim späteren Funktionsaufruf. -
Decorator und Wrapper verwechseln: Der Decorator bekommt die Funktion. Der Wrapper ist häufig das Objekt, das später stattdessen aufgerufen wird.
-
Glauben, ein Decorator müsse eine Funktion zurückgeben: Der Rückgabewert wird an den dekorierten Namen gebunden und kann auch ein anderes Objekt sein.
-
@wrapsvergessen: Dann zeigen Namen, Docstrings, Annotationen und Introspection schnell auf den Wrapper statt auf das ursprüngliche API. -
@wrapsmit perfekter Typisierung verwechseln: Runtime-Metadaten und statische Callable-Typen sind zwei verschiedene Ebenen. -
Den Rückgabewert verlieren: Wenn der Decorator ihn nicht absichtlich verändert, sollte der Wrapper ihn weiterreichen.
-
Exceptions verschlucken: Ein Logging-Decorator sollte Fehler nicht unbemerkt in
NoneoderFalseverwandeln. -
*argsund**kwargsvergessen: Ein allgemeiner Wrapper muss die Parameter der ursprünglichen Funktion weiterreichen können. -
Decorator Factory und Decorator vermischen: Bei
@wiederhole(3)erzeugtwiederhole(3)erst den eigentlichen Decorator. -
Die Reihenfolge mehrerer Decorators verwechseln: Bei
@a @bentsteht gedanklicha(b(funktion)). -
Teure Arbeit während der Decoration ausführen: Decorators auf Modulebene laufen normalerweise schon beim Import.
-
Mutable Closure-State unterschätzen: Er bleibt zwischen Aufrufen erhalten und ist nicht automatisch threadsicher.
-
Einen synchronen Wrapper auf
async deflegen und echte Laufzeitmessung erwarten: Ohneawaitmisst Du im Wesentlichen nur die Erzeugung des Coroutine-Objekts. -
Generatorfunktionen wie normale Funktionen behandeln: Der eigentliche Generator-Body läuft erst beim Iterieren.
-
Properties für überraschend teure oder verändernde Arbeit verwenden: Ein Property-Zugriff sollte sich möglichst wie ein Attribut verhalten.
-
cached_propertymit einem normalenpropertygleichsetzen: Der Wert wird nach dem ersten Zugriff in der Instanz gespeichert und benötigt normalerweise ein__dict__. -
@staticmethodnur verwenden, weil eine Funktion irgendwie zur Datei mit der Klasse gehört: Eine normale Modulfunktion kann klarer sein. -
@classmethodmit einer normalen Factory außerhalb der Klasse verwechseln: Für alternative Konstruktoren istcls(...)gerade wegen Vererbung praktisch. -
Unbeschränkte Caches sorglos einsetzen:
@cachekann in langlebigen Prozessen wachsen und hält Referenzen auf Argumente und Ergebnisse. -
Unhashable Argumente cachen wollen:
cacheundlru_cachebenötigen hashbare Function Arguments. -
Threadsafe Cache mit Exactly-once-Ausführung verwechseln: Zwei Threads können denselben noch nicht gecachten Aufruf gleichzeitig berechnen.
-
Framework-Decorators auf Wrapper reduzieren: Viele registrieren Funktionen oder ersetzen sie durch Framework-Objekte.
-
Zu viel Fachlogik hinter Decorators verstecken: Querschnittsverhalten passt gut; zentrale Business-Logik wird dadurch oft schwerer nachvollziehbar.
Kompakte Übersicht
| Ziel | Werkzeug |
|---|---|
| Funktion als Objekt weiterreichen | Funktionsname ohne () |
| einfachen Decorator anwenden | @decorator |
| gedankliche Langform | funktion = decorator(funktion) |
| allgemeinen Wrapper bauen | *args, **kwargs |
| Rückgabewert erhalten | return funktion(...) |
| Metadaten erhalten | @functools.wraps(funktion) |
| Originalfunktion erreichen | funktion.__wrapped__ |
| konfigurierbaren Decorator bauen | Decorator Factory |
| mehrere Decorators verstehen | a(b(funktion)) |
| State im Wrapper halten | Closure + nonlocal |
| Laufzeit messen | time.perf_counter() |
| Ergebnisse cachen | @cache, @lru_cache(...) |
| berechnetes Attribut | @property |
| einmal berechnetes Attribut | @cached_property |
| alternativer Konstruktor | @classmethod |
| Funktion im Klassennamensraum | @staticmethod |
| Klasse automatisch ergänzen | @dataclass |
| nach Typ dispatchen | @singledispatch |
| Decorator typisieren | ParamSpec + Return-Type-Parameter |
| Async Function einwickeln | async def wrapper(...) |
| Generator einwickeln | Generator-Wrapper + yield from |
Übungen
1. Einen einfachen Decorator schreiben
Schreibe einen Decorator mit_log, der vor jedem Aufruf den Funktionsnamen
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. Die Langform ausschreiben
Was ist die Langform dieses Codes?
@mit_log
def speichern():
print("gespeichert")
Lösung
def speichern():
print(
"gespeichert"
)
speichern = mit_log(
speichern
)
3. Rückgabewerte erhalten
Schreibe einen Decorator, der vor und nach einem Aufruf etwas ausgibt, ohne den Rückgabewert zu verlieren.
Teste ihn mit:
addiere(2, 3)
Lösung
from functools import wraps
def mit_log(funktion):
@wraps(funktion)
def wrapper(
*args,
**kwargs,
):
print("startet")
ergebnis = funktion(
*args,
**kwargs,
)
print("fertig")
return ergebnis
return wrapper
@mit_log
def addiere(
a,
b,
):
return a + b
print(
addiere(
2,
3,
)
)
startet
fertig
5
4. Eine Decorator Factory bauen
Schreibe:
@wiederhole(3)
def klopf():
print("klopf")
so, dass klopf dreimal ausgegeben 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
5. Die Reihenfolge vorhersagen
Was gibt dieser Code aus?
@a
@b
def hallo():
print("hallo")
wenn a und b jeweils vor und nach ihrem inneren Aufruf ihren Buchstaben
ausgeben?
Lösung
Die Decoration entspricht:hallo = a(
b(
hallo
)
)
a davor
b davor
hallo
b danach
a danach
6. Einen Aufrufzähler bauen
Schreibe einen Decorator, der mitzählt, wie häufig eine Funktion aufgerufen wurde.
Lösung
from functools import wraps
def zaehle_aufrufe(funktion):
anzahl = 0
@wraps(funktion)
def wrapper(
*args,
**kwargs,
):
nonlocal anzahl
anzahl += 1
print(
f"Aufruf {anzahl}"
)
return funktion(
*args,
**kwargs,
)
return wrapper
7. Einen 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)
)
8. Eine Property bauen
Schreibe eine Klasse Spieler mit hp und der berechneten 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
9. Einen typisierten Decorator schreiben
Typisiere unter Python 3.12 oder neuer einen Decorator, der Parameter und Rückgabewert der dekorierten Funktion erhält.
Lösung
from collections.abc import Callable
from functools import wraps
def mit_log[
T,
**P,
](
funktion: Callable[P, T],
) -> Callable[P, T]:
@wraps(funktion)
def wrapper(
*args: P.args,
**kwargs: P.kwargs,
) -> T:
print(
funktion.__name__
)
return funktion(
*args,
**kwargs,
)
return wrapper
10. Einen Async-Decorator korrigieren
Warum ist dieser Wrapper für eine Coroutine Function ungeeignet?
def mit_log(funktion):
def wrapper(
*args,
**kwargs,
):
print("start")
ergebnis = funktion(
*args,
**kwargs,
)
print("fertig")
return ergebnis
return wrapper
Lösung
Der Aufruf einer Coroutine Function erzeugt zunächst nur ein Coroutine-Objekt. Der eigentliche Body läuft erst beim `await`. Der Wrapper muss deshalb selbst asynchron sein:from functools import wraps
def mit_log(funktion):
@wraps(funktion)
async def wrapper(
*args,
**kwargs,
):
print("start")
try:
return await funktion(
*args,
**kwargs,
)
finally:
print("fertig")
return wrapper
Weiterlesen
- 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
- Python-Dokumentation:
functools - Python-Dokumentation: Function Definitions und Decorators
- Python-Dokumentation: Class Definitions
- Python-Dokumentation: Descriptor Guide
- Python-Dokumentation:
property - Python-Dokumentation:
classmethod - Python-Dokumentation:
staticmethod - Python-Dokumentation:
dataclasses - Python-Dokumentation:
ParamSpec - PEP 318: Decorators for Functions and Methods
- PEP 3129: Class Decorators
- PEP 612: Parameter Specification Variables
- PEP 695: Type Parameter Syntax
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.