Python Decorators
Erfahre, wie Python-Decorators funktionieren: eigene schreiben, Metadaten mit functools.wraps erhalten, Decorators stapeln und reale Anwendungsfälle.
Ein Decorator ist eine Funktion, die eine andere Funktion umschließt, um deren Verhalten zu erweitern oder zu verändern, ohne den Quellcode zu ändern. Decorators gehören zu den leistungsfähigsten und idiomatischsten Merkmalen von Python – sie sind die Grundlage hinter @staticmethod, @classmethod, @property, @functools.lru_cache und vielen gängigen Mustern in Web-Frameworks.
Diese Seite erklärt, wie Decorators funktionieren, wie man sie von Grund auf selbst schreibt, wie man Argumente an Decorators übergibt, wie man sie stapelt und wann welches Muster am sinnvollsten ist.
Wie Decorators funktionieren
Ein Decorator ist schlicht eine Funktion, die eine andere Funktion als Argument entgegennimmt und eine neue Funktion zurückgibt. Python bietet die @-Syntax als Kurzform dafür:
@shout
def greet(name):
return f"hello, {name}"Das ist genau gleichbedeutend mit:
def greet(name):
return f"hello, {name}"
greet = shout(greet)Die Zeile @shout weist Python an: Nach der Definition von greet wird diese sofort an shout übergeben und der Name greet an das gebunden, was shout zurückgibt. Ab diesem Zeitpunkt läuft jeder Aufruf von greet(...) zunächst durch die Logik von shout.
Den ersten Decorator schreiben
Ein Decorator definiert normalerweise eine innere Wrapper-Funktion, die die ursprüngliche Funktion aufruft und zusätzliches Verhalten darum herum hinzufügt:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet("python")) # HELLO, PYTHONwrapper akzeptiert *args und **kwargs, damit es jede Kombination von Argumenten unverändert an func weiterleitet. Dadurch ist der Decorator mit jeder Funktion kompatibel, unabhängig von ihrer Signatur – eine gute Gewohnheit von Anfang an.
Warum der Wrapper die innere Funktion zurückgeben muss
shout endet mit return wrapper, nicht mit return wrapper(). Das ist beabsichtigt: shout erstellt ein neues aufrufbares Objekt, ruft es aber noch nicht auf. Wenn man versehentlich return wrapper() schreibt, würde der Decorator sofort beim Dekorieren ausgeführt werden und greet wäre an den Rückgabewert von wrapper gebunden – einen String – statt an das aufrufbare Objekt selbst.
Metadaten mit functools.wraps erhalten
Jede Python-Funktion trägt Metadaten: __name__, __doc__, __module__ und mehr. Ohne besondere Vorkehrung ersetzt ein Decorator die ursprüngliche Funktion durch wrapper und verliert all das:
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet.__name__) # wrapper — wrong
print(greet.__doc__) # None — lostAbhilfe schafft das Anwenden von @functools.wraps(func) auf den Wrapper. Dabei werden die Metadaten der ursprünglichen Funktion auf wrapper kopiert:
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet.__name__) # greet
print(greet.__doc__) # Say hello to name.Verwende immer @functools.wraps in jedem selbst geschriebenen Decorator. Ohne es sehen Debugging-Tools, Dokumentationsgeneratoren und Test-Frameworks den falschen Funktionsnamen. Die einzige Ausnahme ist, wenn man die ursprüngliche Identität bewusst verbergen möchte.
Praktische Decorator-Beispiele
Logger
Jeden Aufruf einer Funktion mit ihren Argumenten und dem Rückgabewert protokollieren:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8Timer
Messen, wie lange eine Funktion zur Ausführung braucht:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
total = slow_sum(1_000_000)
print(total) # slow_sum took 0.01xxs then 499999500000time.perf_counter() ist hier die richtige Wahl, da es die höchste verfügbare Auflösung für Kurzzeitsmessungen bietet.
Memoization (Cache)
Den Rückgabewert für jeden eindeutigen Satz von Argumenten zwischenspeichern, sodass die Funktion für dieselbe Eingabe nie zweimal berechnet wird:
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
print(fibonacci(30)) # 832040Für Produktionscode ist das eingebaute @functools.lru_cache oder @functools.cache (Python 3.9+) zu bevorzugen, da diese Sonderfälle, Thread-Sicherheit und Cache-Größenbeschränkungen berücksichtigen. Die oben gezeigte handgefertigte Version ist nützlich, um das Muster zu verstehen.
Zugriffskontrolle
Eine Funktion absichern, sodass sie nur ausgeführt werden kann, wenn eine Bedingung erfüllt ist:
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_authenticated"):
raise PermissionError("Authentication required.")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def get_dashboard(user):
return f"Welcome, {user['name']}!"
guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}
try:
print(get_dashboard(guest))
except PermissionError as e:
print(e) # Authentication required.
print(get_dashboard(admin)) # Welcome, Admin!Decorators mit Argumenten
Manchmal muss ein Decorator zur Dekorationszeit konfiguriert werden – zum Beispiel um eine Funktion eine variable Anzahl von Malen zu wiederholen. Einfache Decorators können keine zusätzlichen Argumente direkt entgegennehmen, weil Python die Funktion übergibt, nicht die Argumente. Die Lösung ist eine Decorator-Factory: eine Funktion, die die Konfiguration entgegennimmt und einen Decorator zurückgibt:
import functools
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say(message):
print(message)
say("hello")
# hello
# hello
# helloVon außen nach innen gelesen: @repeat(3) ruft zunächst repeat(3) auf, was decorator zurückgibt. Python wendet dann decorator auf say an, was wrapper zurückgibt. Damit zeigt say auf wrapper – dasselbe Muster wie zuvor, nur mit einer zusätzlichen Ebene, um n in den Gültigkeitsbereich zu bringen.
Die Verschachtelung kann zunächst abschreckend wirken. Eine mentale Eselsbrücke: Die äußerste Funktion enthält die Konfiguration, die mittlere Funktion die zu dekorierende Funktion und die innerste Funktion den abzufangenden Aufruf.
Mehrere Decorators stapeln
Man kann mehrere Decorators auf eine einzelne Funktion anwenden, indem man @-Zeilen stapelt. Python wendet sie von unten nach oben an – der Decorator, der dem def am nächsten ist, wird zuerst angewendet:
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<b>" + func(*args, **kwargs) + "</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<i>" + func(*args, **kwargs) + "</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # <b><i>Hello, Alice</i></b>Entspricht greet = bold(italic(greet)). italic umschließt zuerst greet, dann umschließt bold das Ergebnis. Die Ausgabe zeigt, dass italic näher am reinen String läuft und bold außen umschließt.
Klassenbasierte Decorators
Auch eine Klasse kann ein Decorator sein – jedes Objekt mit einer __call__-Methode ist aufrufbar. Klassenbasierte Decorators sind nützlich, wenn der Decorator selbst über Aufrufe hinweg einen Zustand aufrechterhalten muss:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
print(say_hello.count) # 2functools.update_wrapper(self, func) übernimmt dieselbe Aufgabe wie @functools.wraps – es kopiert die Metadaten der ursprünglichen Funktion auf die Instanz. Nach der Dekorierung ist say_hello eine CountCalls-Instanz, sodass say_hello.count ein gewöhnlicher Attributzugriff ist.
Wann eine Klasse einem Funktions-Decorator vorzuziehen ist:
- Man benötigt persistenten Zustand (
count,cache, Flags). - Der Decorator hat mehrere Methoden oder Hilfslogik.
- Das dekorierte Objekt soll als spezifischer Typ introspektierbar sein.
Häufige Fallstricke mit Decorators
Die dekorierte Funktion vergessen aufzurufen
Ein häufiger Anfängerfehler ist, den Wrapper zurückzugeben, aber vergessen, func darin aufzurufen:
def broken(func):
def wrapper(*args, **kwargs):
print("before")
# forgot to call func!
return wrapperDie dekorierte Funktion gibt jedes Mal stillschweigend None zurück. Stelle immer sicher, dass wrapper func(*args, **kwargs) aufruft und dessen Ergebnis zurückgibt.
Auf der falschen Ebene dekorieren
Bei parametrisierten Decorators ist es ein häufiger Fehler, den äußeren Aufruf zu vergessen:
# Wrong — 'repeat' receives the function, not a count
@repeat # should be @repeat(3)
def say(msg):
print(msg)Dabei wird say an repeat übergeben, wo n erwartet wird, was beim Aufruf von say zu einem TypeError führt.
Die Reihenfolge von Decorators ist wichtig
Bei gestapelten Decorators verändert die Reihenfolge das Verhalten. @timer über @log_calls auf derselben Funktion misst die Zeit der bereits protokollierten Version, während die umgekehrte Reihenfolge die bereits zeitgesteuerte Version protokolliert. Überlege genau, was jede Schicht sehen soll.
Beziehung zu Closures
Die wrapper-Funktion eines Decorators ist ein Closure – sie erfasst func aus dem umgebenden Gültigkeitsbereich und hält ihn am Leben, auch nachdem die äußere Decorator-Funktion bereits zurückgekehrt ist. Das Verstehen von Closures macht die innere Funktionsweise von Decorators offensichtlich: Das Cell-Objekt, das func enthält, ist genau das, was wrapper ermöglicht, die ursprüngliche Funktion aufzurufen, lange nachdem shout(greet) abgeschlossen wurde.
Zur Syntax von *args und **kwargs, die innerhalb von Wrappern verwendet wird, siehe das entsprechende Kapitel. Zu Lambda-Ausdrücken, die sich gut mit Decorators in höherwertigen Mustern kombinieren lassen, siehe das Lambda-Kapitel.
Kurzübersicht
| Muster | Wann verwenden |
|---|---|
Einfacher wrapper | Verhalten vor/nach einer Funktion hinzufügen |
@functools.wraps | Immer – erhält __name__, __doc__ |
| Decorator-Factory (3 Ebenen) | Decorator muss konfiguriert werden |
| Gestapelte Decorators | Mehrere unabhängige Verhaltensweisen kombinieren |
| Klassenbasierter Decorator | Persistenter Zustand zwischen Aufrufen nötig |
@functools.lru_cache | Reine Funktionen memoizen (eingebaut, produktionsreif) |