Grundlagen

Dekoratoren in Python: was sie sind, wie sie funktionieren und wie Sie einen eigenen schreiben

Aktualisiert: · Skillaria.Top

Ein Dekorator in Python ist eine Funktion, die eine andere Funktion annimmt und eine neue zurückgibt, meist eine Hülle, die vor oder nach dem Aufruf etwas ergänzt: Protokollierung, Zeitmessung, Zwischenspeicherung, Prüfung von Rechten. Die Zeile @decorator über def ist eine Kurzform für func = decorator(func); damit die Hülle Namen und Docstring der ursprünglichen Funktion behält, verwendet man functools.wraps.

Ein Dekorator in Python ist eine Funktion, die eine Funktion entgegennimmt und eine andere zurückgibt, in der Regel eine Hülle um das Original. So ergänzen Sie eine Funktion um Verhalten wie Protokollierung, Zeitmessung, Zwischenspeicher oder Wiederholungsversuche, ohne ihren Code zu ändern. Dekoratoren schreibt man mit der Zeile @name über der Funktionsdefinition, und man trifft sie überall an: in Flask und FastAPI, in pytest und in der Standardbibliothek.

Was ein Dekorator einfach erklärt ist

Stellen Sie sich ein Geschenk mit Geschenkpapier vor. Das Geschenk (die Funktion) bleibt dasselbe, außen liegt aber eine Schicht, die zuerst reagiert. Ein Dekorator ist genau diese Verpackung: Beim Aufruf läuft zuerst der Code der Hülle. Sie ruft die ursprüngliche Funktion auf und kann mit dem Ergebnis etwas anfangen.

Das funktioniert, weil Funktionen in Python gewöhnliche Objekte sind. Man kann sie an andere Funktionen übergeben, aus Funktionen zurückgeben und innerhalb von Funktionen definieren. Auf diesen drei Eigenschaften beruhen Dekoratoren.

Wie ein Dekorator funktioniert: @ ist syntaktischer Zucker

Schauen wir uns einen einfachen Dekorator an, der vor und nach dem Aufruf eine Meldung ausgibt:

def log_calls(func):
    def wrapper():
        print(f"Rufe {func.__name__} auf")
        result = func()
        print(f"{func.__name__} beendet")
        return result
    return wrapper

@log_calls
def hello():
    print("Hallo!")

hello()
Rufe hello auf
Hallo!
hello beendet

Die Zeile @log_calls über def ist gleichbedeutend mit dem folgenden Ausdruck nach der Definition:

def hello():
    print("Hallo!")

hello = log_calls(hello)

Der Name hello verweist jetzt auf wrapper. Die ursprüngliche Funktion lebt innerhalb des Closures weiter, denn wrapper erinnert sich an die Variable func aus der äußeren Funktion. Wichtig ist: Der Dekorator wird nur einmal angewendet, beim Ausführen von def, also beim Import des Moduls. Die Hülle dagegen läuft bei jedem Aufruf.

Closures und Dekoratoren

Ein Closure ist eine innere Funktion, die sich Variablen der äußeren Funktion merkt, auch nachdem diese beendet ist. Ohne Closures gäbe es keine Dekoratoren: So erhält wrapper Zugriff auf func. Im Closure kann auch ein Zustand gespeichert werden, etwa ein Zähler für Aufrufe:

def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: Aufruf Nr. {calls}")
        return func(*args, **kwargs)
    return wrapper

Einen Dekorator in Python schreiben: das universelle Muster

Das erste Beispiel hat zwei Schwächen: Die Hülle nimmt keine Argumente entgegen, und sie überschreibt den Namen der ursprünglichen Funktion. Das folgende Muster sollten Sie als Ausgangspunkt für jeden Dekorator verwenden:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Code vor dem Aufruf
        result = func(*args, **kwargs)
        # Code nach dem Aufruf
        return result
    return wrapper

Drei Regeln des Musters:

  1. *args, **kwargs — die Hülle nimmt beliebige Argumente entgegen und reicht sie unverändert weiter.
  2. return result — das Ergebnis muss zurückgegeben werden, sonst liefert die dekorierte Funktion immer None.
  3. @functools.wraps(func) — überträgt die Metadaten der ursprünglichen Funktion auf die Hülle.

Ein Beispiel für einen Dekorator, der die Laufzeit misst:

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__} dauerte {elapsed:.3f} s")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)   # slow_sum dauerte 0.142 s

Wozu functools.wraps dient

Ohne wraps werden Name, Docstring und andere Attribute der dekorierten Funktion ersetzt, nach außen sichtbar ist dann wrapper:

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """Addiert zwei Zahlen."""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None

Mit @functools.wraps(func) bekommt die Hülle __name__, __doc__, __module__, __qualname__ und __annotations__ der Originalfunktion, und im Attribut __wrapped__ steht ein Verweis auf das Original. Das ist wichtig für Fehlersuche und Tracebacks, für help(), für Frameworks, die Funktionen über ihren Namen registrieren, und für inspect.signature, das die echte Signatur anzeigt.

Dekorator mit Parametern

Manchmal braucht ein Dekorator Einstellungen, zum Beispiel @retry(times=3) oder @app.route("/"). Dafür fügt man eine weitere Verschachtelungsebene hinzu. Die äußere Funktion nimmt die Parameter entgegen und gibt den eigentlichen Dekorator zurück. Diese Konstruktion heißt Dekorator-Fabrik.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping():
    print("ping")

ping()   # ping, ping, ping — drei Zeilen

Die Reihenfolge der Aufrufe: repeat(times=3) liefert decorator, dieser wird auf ping angewendet und gibt wrapper zurück. Anders gesagt: @repeat(times=3) bedeutet ping = repeat(times=3)(ping).

Ein praktisches Beispiel ist die Wiederholung bei einem Fehler:

import functools
import time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
    ...

Mehrere Dekoratoren auf einer Funktion

Dekoratoren lassen sich übereinander stapeln. Angewendet werden sie von unten nach oben: Der Dekorator, der dem def am nächsten steht, kommt zuerst.

@timer
@log_calls
def report():
    ...

# dasselbe:
report = timer(log_calls(report))

Beim Aufruf läuft die äußere Schicht (timer) zuerst, deshalb misst sie auch die Laufzeit von log_calls. Die Reihenfolge ist wichtig, etwa wenn ein Dekorator Zugriffsrechte prüft und ein anderer Ergebnisse zwischenspeichert.

Eingebaute und Standard-Dekoratoren in Python

Viele nützliche Dekoratoren gibt es bereits in der Sprache und in der Standardbibliothek:

DekoratorWas er macht
@propertymacht eine Methode zu einem Attribut nur zum Lesen; mit @x.setter wird daraus ein kontrolliertes Attribut
@staticmethodMethode ohne self, eine gewöhnliche Funktion im Namensraum der Klasse
@classmethoddie Methode erhält die Klasse cls statt einer Instanz — so schreibt man alternative Konstruktoren
@functools.cache, @functools.lru_cachespeichern Ergebnisse nach Argumenten zwischen
@functools.wrapskopiert die Metadaten einer Funktion auf die Hülle
@dataclasses.dataclasserzeugt __init__, __repr__ und __eq__ für eine Klasse
@contextlib.contextmanagermacht aus einem Generator einen Kontextmanager für with

Zwischenspeichern: functools.cache und lru_cache

Ein Cache-Dekorator merkt sich das Ergebnis für jede Kombination von Argumenten und liefert es bei einem erneuten Aufruf ohne Neuberechnung zurück. Ein klassisches Beispiel sind rekursive Fibonacci-Zahlen, die ohne Cache exponentiell lange brauchen:

import functools

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))   # 354224848179261915075

functools.cache (seit Python 3.9) speichert alle Ergebnisse ohne Begrenzung. @functools.lru_cache(maxsize=128) behält nur die letzten Einträge und verdrängt ältere, was für lange laufende Dienste sicherer ist. Die Statistik zeigt fib.cache_info(), geleert wird der Cache mit fib.cache_clear(). Argumente solcher Funktionen müssen hashbar sein: Eine Liste als Argument führt zu TypeError: unhashable type: 'list'. Außerdem sollte man nur reine Funktionen cachen, also solche, die bei gleichen Argumenten immer dasselbe Ergebnis liefern und keine Nebenwirkungen haben.

Was der Dekorator property macht

@property erlaubt, auf eine Methode wie auf ein Attribut zuzugreifen, ohne Klammern, und bei Bedarf das Zuweisen zu kontrollieren:

class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Der Kontostand darf nicht negativ sein")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError

staticmethod und classmethod

class Money:
    def __init__(self, kopecks):
        self.kopecks = kopecks

    @classmethod
    def from_rubles(cls, rubles):
        return cls(round(rubles * 100))

    @staticmethod
    def is_valid(value):
        return value >= 0

m = Money.from_rubles(12.5)
print(m.kopecks, Money.is_valid(-1))   # 1250 False

Gerade @classmethod erlaubt es, mit der Klasse selbst zu arbeiten statt mit ihren Instanzen: from_rubles erzeugt korrekt auch ein Objekt einer Unterklasse.

Dekoratoren für Klassen und Klassen als Dekoratoren

Einen Dekorator kann man auch auf eine Klasse anwenden: Er erhält die Klasse und gibt eine Klasse zurück, dieselbe oder eine veränderte. So funktioniert @dataclass. Ein einfaches Beispiel ist ein Register von Klassen:

registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': <class '__main__.CsvExporter'>}

Umgekehrt kann auch eine Klasse als Dekorator dienen. Geeignet ist jedes Objekt mit einer Methode __call__. Den Zustand speichert man bequem in Attributen der Instanz:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def tick():
    pass

tick(); tick()
print(tick.calls)   # 2

Asynchroner Dekorator

Für Funktionen mit async def muss die Hülle ebenfalls eine Koroutine sein und das Ergebnis mit await abwarten. Andernfalls wird ein nicht ausgeführtes Koroutinenobjekt zurückgegeben:

import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"Start {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"Fertig {func.__name__}")
        return result
    return wrapper

Wozu Dekoratoren in Python dienen

  • Querschnittslogik — Protokollierung, Metriken, Zeitmessung, ohne Code in jede Funktion zu kopieren.
  • Zwischenspeicherung — @functools.cache für teure reine Berechnungen.
  • Prüfungen — Zugriffsrechte, Validierung von Argumenten, Autorisierung in Web-Handlern.
  • Registrierung — Routen in Flask und FastAPI (@app.get("/")), Fixtures in pytest.
  • Zuverlässigkeit — Wiederholungsversuche, Timeouts, Fehlerbehandlung.

Der größte Vorteil ist die Trennung der Verantwortlichkeiten: Eine Funktion erledigt ihre Aufgabe, und Zusatzverhalten wird mit einer Zeile hinzugefügt und genauso leicht wieder entfernt.

Typische Fehler

  • Die Hülle gibt das Ergebnis nicht zurück — die Funktion liefert nach dem Dekorieren None.
  • Keine *args, **kwargs — TypeError beim Aufruf mit Argumenten.
  • wraps vergessen — falsche __name__ und Docstrings, Verwirrung in den Logs.
  • Klammern bei der Fabrik fehlen — @repeat statt @repeat(3): Die Funktion landet im Parameter times.
  • Schwerer Code im Körper des Dekorators — er läuft beim Import des Moduls, nicht beim Aufruf.

Fazit

Dekoratoren in Python sind Funktionen höherer Ordnung: Sie nehmen eine Funktion entgegen und geben eine Hülle zurück, und das @ ist nur eine kurze Schreibweise für eine Zuweisung. Beginnen Sie mit dem Muster aus *args, **kwargs, return result und functools.wraps, und für Einstellungen fügen Sie eine Fabrikebene hinzu. Funktionen, Closures und Annotationen, auf denen Dekoratoren beruhen, werden im Python-Kurs praktisch durchgearbeitet.

Häufige Fragen

Kann man einen Dekorator wieder von einer Funktion entfernen?

Wenn der Dekorator functools.wraps verwendet, ist das Original über das Attribut __wrapped__ erreichbar: func.__wrapped__(...) ruft die Funktion ohne die Hülle auf. Sonst bleibt nur, die Zeile mit @ aus dem Code zu entfernen.

Wann läuft der Code eines Dekorators?

Der Körper des Dekorators selbst läuft einmal, bei der Definition der Funktion, meist beim Import des Moduls. Der Code innerhalb von wrapper läuft bei jedem Aufruf der dekorierten Funktion.

Kann man Methoden einer Klasse dekorieren?

Ja, ein gewöhnlicher Dekorator mit *args, **kwargs funktioniert auch bei Methoden. self steht dann einfach als erstes Element in args.

Worin unterscheidet sich ein Dekorator von Vererbung?

Vererbung verändert das Verhalten einer Klasse über eine Hierarchie. Ein Dekorator umhüllt eine bestimmte Funktion oder Klasse von außen und lässt sich ohne Änderung des Codes mit anderen Dekoratoren kombinieren.

Das Thema im Video lernen

Dieses Thema wird im Kurs „Python 3.15: от новичка до профи“ ausführlich mit Praxis behandelt.

Zum Kurs