Wenn du Flask oder FastAPI länger als eine Woche verwendet hast, hast du bereits Dekoratoren benutzt — @app.route,
@login_required, @pytest.mark.parametrize. Sie fühlen sich beim ersten Mal wie Magie an, und dann
erklärt jemand, was wirklich passiert, und es klickt sofort. Ein Dekorator ist nur eine Funktion, die eine andere Funktion umhüllt. Die @-Syntax ist reiner syntaktischer Zucker — @my_decorator über einer
Funktionsdefinition ist genau äquivalent zum Schreiben von func = my_decorator(func) danach. Das ist das
ganze Geheimnis. Der Rest sind nur Muster, die auf dieser einen Idee aufbauen. Dieser Artikel baut das mentale Modell von
Grund auf, dann geht er durch die Muster, die du wirklich verwenden wirst: @functools.wraps, Dekoratoren mit
Argumenten, klassenbasierte Dekoratoren und eine Handvoll realer Beispiele einschließlich @lru_cache,
@dataclass und einem ordentlichen @retry mit exponentiellem Backoff.
Funktionen sind Objekte erster Klasse
Bevor Dekoratoren Sinn ergeben, musst du eine Python-Tatsache fest im Griff haben: Funktionen sind Objekte. Du kannst sie Variablen zuweisen, als Argumente an andere Funktionen übergeben, aus Funktionen zurückgeben, und in Listen oder Dicts speichern. Nichts Besonderes passiert nur deshalb, weil etwas eine Funktion ist.
def greet(name: str) -> str:
return f"Hello, {name}"
# Einer Variablen zuweisen — kein () bedeutet, wir rufen nicht auf, nur eine Referenz
say_hello = greet
print(say_hello("Alice")) # 'Hello, Alice'
# Eine Funktion als Argument übergeben
def run_twice(fn, value):
return fn(value), fn(value)
run_twice(greet, "Bob") # ('Hello, Bob', 'Hello, Bob')
# Eine Funktion aus einer anderen Funktion zurückgeben — das ist eine "Fabrik"
def make_prefixer(prefix: str):
def prefixed_greet(name: str) -> str:
return f"{prefix}, {name}"
return prefixed_greet
morning_hello = make_prefixer("Good morning")
morning_hello("Carol") # 'Good morning, Carol'Die innere Funktion prefixed_greet "schließt über" die Variable prefix aus dem
umgebenden Scope — selbst nach der Rückgabe von make_prefixer hat die innere Funktion noch Zugriff
auf prefix. Das ist ein
Closure,
und es ist der Mechanismus, der Dekoratoren zum Laufen bringt. Die
Python-Docs zu Scoping-Regeln
erklären das im Detail, wenn du das vollständige Bild willst.
Einen Dekorator von Grund auf bauen
Ein Dekorator ist eine Funktion, die eine Funktion nimmt und eine (meist modifizierte) Funktion zurückgibt. Das klassische erste Beispiel ist ein Timing-Dekorator — er umhüllt jede Funktion und protokolliert, wie lange sie brauchte.
import time
import functools
def timer(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{fn.__name__} finished in {elapsed:.4f}s")
return result
return wrapper
@timer
def fetch_user_records(db, user_id: int):
"""Fetch all records for a given user from the database."""
return db.query("SELECT * FROM records WHERE user_id = ?", user_id)
# fetch_user_records aufzurufen ist jetzt: timer(fetch_user_records)(db, 42)
# Das ist genau das, wozu @timer entcuckertEinige Dinge zu beachten: Der Wrapper verwendet *args, **kwargs, sodass er jede Kombination von Argumenten an die ursprüngliche Funktion weiterleiten kann, ohne ihre Signatur zu kennen. Er erfasst den Rückgabewert in result und gibt ihn zurück, sodass die umhüllte Funktion aus der Sicht des Aufrufers noch identisch verhält — sie hat nur einen zusätzlichen Print-Nebeneffekt. Entferne das Timing, und du hast das Skelett von fast jedem Dekorator, den du je schreiben wirst.
@some_decorator über einer
Funktionsdefinition siehst, ersetze es gedanklich durch fn = some_decorator(fn), geschrieben unmittelbar
nach dem def-Block. Die beiden sind genau äquivalent. Es gibt keine Magie — es ist ein Funktionsaufruf.Warum du @functools.wraps verwenden musst
Im obigen Beispiel gibt es eine @functools.wraps(fn)-Zeile auf dem Wrapper. Das ist nicht
optional. Ohne sie verliert deine dekorierte Funktion ihre Identität — ihre __name__,
__doc__ und __qualname__-Attribute werden alle durch die der inneren
wrapper-Funktion ersetzt. Das verursacht subtile Fehler in einigen realen Situationen:
- Docstrings verschwinden.
help(fetch_user_records)zeigt die leere Docstring des Wrappers statt"Fetch all records for a given user...". - Stack Traces lügen. Wenn eine Ausnahme innerhalb der umhüllten Funktion ausgelöst wird, zeigt das Traceback
wrapperstatt des echten Funktionsnamens — schwer zu debuggen. - Introspektion bricht. Tools wie pytest, Flasks Routing-System und
inspect.signature()verlassen sich alle auf__name__und__wrapped__. Flasks Router wirft einen Fehler, wenn zwei Routen denselben (Wrapper-)Namen teilen. functools.lru_cacheund ähnliche Tools verwenden die Identität der Funktion für Cache-Keying — ohnewrapskannst du überraschende Cache-Kollisionen bekommen.
import functools
# Ohne @functools.wraps — kaputt
def bad_timer(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@bad_timer
def process_batch(batch_id: int):
"""Process a single batch job."""
pass
print(process_batch.__name__) # 'wrapper' ← falsch
print(process_batch.__doc__) # None ← Docstring weg
# Mit @functools.wraps — korrekt
def good_timer(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@good_timer
def process_batch(batch_id: int):
"""Process a single batch job."""
pass
print(process_batch.__name__) # 'process_batch' ← korrekt
print(process_batch.__doc__) # 'Process a single batch job.' ← erhaltenfunctools.wraps
ist selbst ein Dekorator — er kopiert __module__, __name__, __qualname__,
__annotations__, __doc__, und setzt __wrapped__ auf die ursprüngliche Funktion.
Verwende es bei jeder Wrapper-Funktion, Punkt. Es gibt keinen Grund, es nicht zu tun.
Dekoratoren mit Argumenten
Ein einfacher Dekorator nimmt eine Funktion und gibt eine Funktion zurück. Ein Dekorator mit Argumenten braucht
eine Ebene mehr: eine Funktion, die die Argumente nimmt und einen Dekorator zurückgibt. Das sind drei
Verschachtelungsebenen, und es verwirrt fast jeden beim ersten Mal. Hier ist ein @retry-Dekorator, der
eine Funktion bei Ausnahmen bis zu max_attempts Mal wiederholt:
import functools
import time
def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0):
"""
Retry a function on exception with exponential backoff.
Usage:
@retry(max_attempts=5, delay=0.5, backoff=2.0)
def call_external_api(endpoint: str) -> dict:
...
"""
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
current_delay = delay
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return fn(*args, **kwargs)
except Exception as exc:
last_exception = exc
if attempt < max_attempts:
print(
f"{fn.__name__}: attempt {attempt}/{max_attempts} failed "
f"({exc!r}), retrying in {current_delay:.1f}s..."
)
time.sleep(current_delay)
current_delay *= backoff
else:
print(f"{fn.__name__}: all {max_attempts} attempts failed.")
raise last_exception
return wrapper
return decorator
@retry(max_attempts=4, delay=0.5, backoff=2.0)
def fetch_price_data(ticker: str) -> dict:
"""Fetch stock price data from external API."""
response = requests.get(f"https://api.example.com/prices/{ticker}", timeout=5)
response.raise_for_status()
return response.json()Die Aufrufkette, wenn Python @retry(max_attempts=4, delay=0.5, backoff=2.0) verarbeitet:
zuerst wird retry(max_attempts=4, delay=0.5, backoff=2.0) aufgerufen und gibt decorator zurück.
Dann wird decorator(fetch_price_data) aufgerufen und gibt wrapper zurück. Schließlich
wird fetch_price_data auf wrapper neu gebunden. Also ist @retry(...)
fetch_price_data = retry(...)(fetch_price_data) — drei Aufrufe, zwei Ebenen des Umhüllens.
Sobald du dieses Muster siehst, hören Dekoratorfabriken auf, verwirrend zu sein.
Klassenbasierte Dekoratoren
Du kannst einen Dekorator auch als Klasse implementieren, indem du __call__ definierst. Das ist nützlich,
wenn der Dekorator zwischen Aufrufen Zustand halten muss — ein Aufrufzähler, ein Cache, Verbindungspools — da
Instanzvariablen ein natürlicheres Zuhause für diesen Zustand sind als Closure-Variablen.
import functools
import time
class RateLimiter:
"""
Decorator that limits how often a function can be called.
Raises RuntimeError if the function is called within `min_interval` seconds
of the previous call.
"""
def __init__(self, min_interval: float):
self.min_interval = min_interval
self._last_called: float = 0.0
def __call__(self, fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
now = time.monotonic()
since_last = now - self._last_called
if since_last < self.min_interval:
wait = self.min_interval - since_last
raise RuntimeError(
f"{fn.__name__} called too soon — "
f"wait {wait:.2f}s before calling again."
)
self._last_called = now
return fn(*args, **kwargs)
return wrapper
# Verwendung — ein Aufruf alle 2 Sekunden
@RateLimiter(min_interval=2.0)
def send_sms_alert(phone: str, message: str) -> None:
"""Send an SMS alert via the gateway API."""
sms_gateway.send(phone, message)@RateLimiter(min_interval=2.0) funktioniert genau wie eine Dekoratorfabrik:
RateLimiter(2.0) erstellt eine Instanz, dann wird diese Instanz mit
send_sms_alert aufgerufen, weil sie __call__ hat. Die Instanz speichert
_last_called als Attribut — kein Jonglieren mit Closure-Variablen nötig. Sieh dir
PEP 318 (den ursprünglichen
Dekorator-Vorschlag) für die Designbegründung hinter der @-Syntax an, und
PEP 614 für die entspannte
Dekorator-Grammatik, die in Python 3.9 eingeführt wurde.
Reale Dekoratoren aus der Standardbibliothek
Bevor du deinen eigenen schreibst, prüfe ob die stdlib bereits hat, was du brauchst. Drei Dekoratoren in
functools
tauchen ständig in Produktions-Python-Code auf.
from functools import lru_cache, cache
import requests
# @cache — unbegrenzte Memoisation (Python 3.9+)
# Cached jeden einzigartigen Satz von Argumenten für immer.
# Gut für reine Funktionen mit einem kleinen Eingabebereich.
@cache
def get_country_name(country_code: str) -> str:
"""Look up a country name from ISO 3166 code. Cached after first call."""
response = requests.get(f"https://restcountries.com/v3.1/alpha/{country_code}")
return response.json()[0]["name"]["common"]
get_country_name("DE") # trifft die API
get_country_name("DE") # aus dem Cache, kein Netzwerkaufruf
# @lru_cache(maxsize=N) — begrenzter LRU-Cache
# Räumt zuletzt-verwendete Einträge aus, wenn der Cache `maxsize` erreicht.
# Besser wenn der Eingabebereich groß ist und Speicher ein Problem ist.
@lru_cache(maxsize=256)
def compute_discount(base_price: float, tier: str) -> float:
"""Schwere Berechnung — Preis variiert nach Tier. Cache die Top 256 Kombinationen."""
discount_table = load_discount_table() # teurer DB-Aufruf
rate = discount_table.get(tier, 0.0)
return round(base_price * (1 - rate), 2)
# Cache-Performance inspizieren
print(compute_discount.cache_info())
# CacheInfo(hits=142, misses=14, maxsize=256, currsize=14)@dataclass ist in einer anderen Kategorie — es ist ein Klassen-Dekorator, der automatisch
__init__, __repr__ und __eq__ aus deinen Feld-Annotationen generiert. Er schneidet
erheblich viel Boilerplate für datenhaltende Klassen heraus:
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime
@dataclass
class WebhookEvent:
event_type: str
source_id: int
payload: dict
received_at: datetime = field(default_factory=datetime.utcnow)
retry_count: int = 0
error_message: Optional[str] = None
# @dataclass generiert all das kostenlos:
# - __init__(self, event_type, source_id, payload, received_at=..., retry_count=0, error_message=None)
# - __repr__ das alle Felder zeigt
# - __eq__ das Feld für Feld vergleicht
event = WebhookEvent(event_type="order.created", source_id=9912, payload={"order_id": 44501})
print(event)
# WebhookEvent(event_type='order.created', source_id=9912, payload={...}, retry_count=0, ...)Der @property-Dekorator verwandelt eine Methode in einen Attribut-Stil-Accessor — Aufrufer
lesen user.display_name statt user.get_display_name(). Kombiniere es mit
@property.setter um beim Schreiben zu validieren:
class UserProfile:
def __init__(self, first_name: str, last_name: str, email: str):
self._first_name = first_name
self._last_name = last_name
self._email = email.strip().lower()
@property
def display_name(self) -> str:
return f"{self._first_name} {self._last_name}"
@property
def email(self) -> str:
return self._email
@email.setter
def email(self, value: str) -> None:
if "@" not in value:
raise ValueError(f"Invalid email address: {value!r}")
self._email = value.strip().lower()
profile = UserProfile("Alice", "Smith", " [email protected] ")
print(profile.display_name) # 'Alice Smith'
print(profile.email) # '[email protected]'
profile.email = "[email protected]" # Setter führt Validierung aus
profile.email = "not-an-email" # wirft ValueErrorEin @require_auth-Dekorator für Route-Handler
Authentifizierungsprüfungen in Web-Frameworks sind ein Paradebeispiel für Dekoratoren. Statt den "ist der Benutzer eingeloggt?"-Check am Anfang jedes Route-Handlers zu duplizieren, schreibst du ihn einmal als Dekorator und wendest ihn dort an, wo er benötigt wird. Hier ist das Muster, geschrieben für Flask, aber auf jedes Framework übertragbar:
import functools
from flask import request, jsonify, g
def require_auth(fn):
"""
Decorator that validates a Bearer token before the route handler runs.
Sets g.current_user on success; returns 401 JSON on failure.
"""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return jsonify({"error": "Missing or malformed Authorization header"}), 401
token = auth_header[len("Bearer "):]
user = verify_token(token) # deine Token-Validierungslogik
if user is None:
return jsonify({"error": "Invalid or expired token"}), 401
g.current_user = user # für den Route-Handler verfügbar
return fn(*args, **kwargs)
return wrapper
# Verwendung — die Auth-Prüfung läuft vor dem Handler-Body
@app.route("/api/v1/reports/<int:report_id>")
@require_auth
def get_report(report_id: int):
report = Report.query.get_or_404(report_id)
if report.owner_id != g.current_user.id:
return jsonify({"error": "Forbidden"}), 403
return jsonify(report.to_dict())Beachte die Reihenfolge: @app.route geht zuerst (äußerster), @require_auth geht
zweiter. Das ist wichtig — sieh dir den nächsten Abschnitt an. Das Muster lässt sich natürlich erweitern: Du könntest
eine Dekoratorfabrik @require_role("admin") hinzufügen, die g.current_user.role prüft,
nachdem @require_auth bereits überprüft hat, dass der Benutzer existiert.
Dekoratoren stapeln — Reihenfolge ist wichtig
Wenn du mehrere Dekoratoren stapelst, werden sie von unten nach oben angewendet (der Dekorator nächst zur Funktion wird zuerst angewendet), aber sie führen von oben nach unten aus, wenn die Funktion aufgerufen wird. Das überrascht die Leute.
@decorator_a
@decorator_b
@decorator_c
def my_function():
pass
# Das ist genau äquivalent zu:
my_function = decorator_a(decorator_b(decorator_c(my_function)))
# Wenn my_function() aufgerufen wird:
# 1. decorator_a's wrapper läuft zuerst (äußerster)
# 2. decorator_b's wrapper läuft zweiter
# 3. decorator_c's wrapper läuft dritter (innerster, nächst zur echten Funktion)
# 4. Der echte my_function-Body läuft
# 5. Abwickeln: decorator_c → decorator_b → decorator_a# Praktisches Beispiel: Reihenfolge ist wichtig für @timer und @retry
# Wenn timer am äußersten ist, misst er die Gesamtzeit einschließlich Retry-Wartezeiten.
# Wenn retry am äußersten ist, misst er nur den letzten erfolgreichen Aufruf.
@timer # misst: Gesamtzeit über alle Retry-Versuche + Sleep
@retry(max_attempts=3, delay=1.0)
def sync_with_partner_api(partner_id: int) -> dict:
...
# vs.
@retry(max_attempts=3, delay=1.0) # misst: nur den letzten erfolgreichen Aufruf
@timer
def sync_with_partner_api(partner_id: int) -> dict:
...
# Normalerweise willst du @timer am äußersten — es sagt dir die echten Wanduhrkosten der Operation.
# Denke daran, was "diese Funktion aufrufen" für den Aufrufer bedeutet, dann wrappe in dieser Reihenfolge.@functools.wraps korrekt verwendet hast, spiegeln die Namen
die ursprünglichen Funktionen wider. Wenn du ein Meer von wrapper-Frames siehst, hat jemand
@functools.wraps vergessen. Der
Real Python-Dekorator-Primer
hat eine gute Anleitung zum Debuggen gestapelter Dekoratoren.Zusammenfassung
Das mentale Modell, das du mitnehmen solltest: @decorator ist fn = decorator(fn).
Alles andere — Dekoratoren mit Argumenten, klassenbasierte Dekoratoren, gestapelte Dekoratoren — ist eine Variation dieser
einen Substitution. Verwende @functools.wraps bei jedem inneren Wrapper, leite immer
*args, **kwargs an die umhüllte Funktion weiter, und gib ihr Ergebnis zurück. Dekoratoren mit Argumenten
brauchen drei Verschachtelungsebenen: die Argumentfunktion, den Dekorator und den Wrapper. Klassenbasierte Dekoratoren
sind der richtige Griff, wenn dein Dekorator Zustand zwischen Aufrufen halten muss.
Für weitere Lektüre: Der
Python-Glossar-Eintrag zu Dekoratoren
ist kurz aber präzise. Der ursprüngliche Dekorator-Vorschlag,
PEP 318, ist es wert gelesen zu werden
für den Kontext, warum die @-Syntax gegenüber Alternativen gewählt wurde. Wenn du Dekoratoren
in einer Codebase verwendest, die auch viel Datenverarbeitung macht — Dateien lesen, Datensätze transformieren — passen die
Muster hier natürlich mit dem zusammen, was in
Python File Handling behandelt wird. Und wenn du Dekoratoren verwendest um
Sammlungen zu transformieren oder Nachschlagestrukturen aufzubauen,
deckt Python List Comprehensions die
Datentransformationsseite dieses Bildes ab. Wenn deine dekorierten Funktionen JSON-Ausgaben zurückgeben und du sie schnell
inspizieren willst, ist der JSON Formatter auf dieser Seite praktisch.