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.

python
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.

python
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 entcuckert

Einige 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.

Die Entcuckerungs-Regel: Immer wenn du @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 wrapper statt 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_cache und ähnliche Tools verwenden die Identität der Funktion für Cache-Keying — ohne wraps kannst du überraschende Cache-Kollisionen bekommen.
python
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.'  ← erhalten

functools.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:

python
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.

python
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.

python
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:

python
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:

python
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 ValueError

Ein @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:

python
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.

python
@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
python
# 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.
Stack-Trace-Tipp: Wenn eine dekorierte Funktion eine Ausnahme auslöst, zeigt das Traceback jeden Wrapper im Aufrufstack. Wenn du @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.