Dekoratoren in Python: Funktionen elegant erweitern

Dekoratoren gehören zu den mächtigsten Werkzeugen in Python: Mit ihnen erweiterst du Funktionen um neue Fähigkeiten, ohne ihren Code anzufassen. Hier lernst du Schritt für Schritt, wie sie funktionieren.

Teilen

Du hast sie bestimmt schon gesehen: Zeilen, die mit einem @ beginnen und direkt über einer Funktionsdefinition stehen. Ob @app.route in Flask, @pytest.fixture in Tests oder @property in Klassen – Dekoratoren sind überall in Python. Viele Einsteiger nutzen sie, ohne genau zu verstehen, was dahintersteckt. Dabei ist das Konzept gar nicht so kompliziert, wenn man es einmal in Ruhe auseinandernimmt. In diesem Beitrag baust du deinen ersten eigenen Dekorator und lernst, wie du damit Code sauberer und wiederverwendbarer machst.

Funktionen sind Objekte

Um Dekoratoren zu verstehen, brauchst du eine wichtige Grundlage: In Python sind Funktionen ganz normale Objekte. Du kannst sie einer Variablen zuweisen, in Listen speichern oder als Argument an andere Funktionen übergeben – genau wie Zahlen oder Strings.

def begruessung(name):
    return f"Hallo, {name}!"

# Funktion einer Variablen zuweisen (ohne Klammern!)
sag_hallo = begruessung
print(sag_hallo("Anna"))  # Hallo, Anna!

# Funktion als Argument übergeben
def zweimal(func, wert):
    return func(func(wert))

print(zweimal(str.upper, "hi"))  # HI

Eine Funktion, die andere Funktionen entgegennimmt oder zurückgibt, nennt man Higher-Order Function. Genau das ist ein Dekorator: eine Funktion, die eine Funktion bekommt und eine (meist erweiterte) Funktion zurückgibt.

Dein erster eigener Dekorator

Bauen wir einen Dekorator, der protokolliert, wann eine Funktion aufgerufen wird. Das Muster besteht aus drei Teilen: der äußeren Dekorator-Funktion, einer inneren wrapper-Funktion und dem return wrapper am Ende.

def logge_aufruf(func):
    def wrapper(*args, **kwargs):
        print(f"Rufe {func.__name__} auf...")
        ergebnis = func(*args, **kwargs)
        print(f"{func.__name__} ist fertig.")
        return ergebnis
    return wrapper

@logge_aufruf
def addiere(a, b):
    return a + b

print(addiere(3, 4))
# Rufe addiere auf...
# addiere ist fertig.
# 7

Die Zeile @logge_aufruf ist dabei nur eine Abkürzung – der sogenannte syntaktische Zucker – für addiere = logge_aufruf(addiere). Python ersetzt deine Funktion also durch die wrapper-Funktion, die vor und nach dem eigentlichen Aufruf zusätzlichen Code ausführt. Durch *args und **kwargs funktioniert der Wrapper mit beliebigen Argumenten.

Metadaten retten mit functools.wraps

Unser Dekorator hat einen Schönheitsfehler: Weil addiere jetzt in Wahrheit wrapper ist, gehen Name und Docstring der Originalfunktion verloren. addiere.__name__ liefert plötzlich "wrapper" – das verwirrt Debugger, Dokumentationstools und dich selbst. Die Lösung liefert die Standardbibliothek mit functools.wraps:

import functools
import time

def stoppuhr(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        ergebnis = func(*args, **kwargs)
        dauer = time.perf_counter() - start
        print(f"{func.__name__} lief {dauer:.4f} Sekunden")
        return ergebnis
    return wrapper

@stoppuhr
def langsame_berechnung(n):
    """Summiert die Zahlen von 0 bis n."""
    return sum(range(n))

langsame_berechnung(10_000_000)
print(langsame_berechnung.__name__)  # langsame_berechnung – korrekt!

Merk dir als Faustregel: Jeder Dekorator, den du schreibst, sollte @functools.wraps(func) auf seinem Wrapper haben. Es kostet eine Zeile und erspart dir später seltsame Überraschungen.

Dekoratoren mit eigenen Argumenten

Manchmal soll ein Dekorator konfigurierbar sein, etwa @wiederhole(3). Dafür brauchst du eine zusätzliche Ebene: eine Funktion, die die Argumente entgegennimmt und den eigentlichen Dekorator zurückgibt.

import functools

def wiederhole(anzahl):
    def dekorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            ergebnis = None
            for _ in range(anzahl):
                ergebnis = func(*args, **kwargs)
            return ergebnis
        return wrapper
    return dekorator

@wiederhole(3)
def gruss():
    print("Hallo!")

gruss()
# Hallo!
# Hallo!
# Hallo!

Das sieht auf den ersten Blick verschachtelt aus, folgt aber immer demselben Schema von außen nach innen: Argumente → Funktion → Wrapper. Wenn du dieses Muster einmal abgespeichert hast, kannst du es für Retry-Logik, Zugriffskontrolle oder Rate Limiting wiederverwenden.

Nützliche Dekoratoren aus der Standardbibliothek

Bevor du alles selbst baust, lohnt ein Blick auf das, was Python schon mitbringt:

  • @functools.lru_cache speichert Rückgabewerte zwischen und beschleunigt teure Funktionen mit wiederkehrenden Argumenten enorm.
  • @property macht eine Methode wie ein Attribut nutzbar – ideal für berechnete Werte in Klassen.
  • @staticmethod und @classmethod steuern, ob eine Methode Zugriff auf Instanz oder Klasse braucht.
  • @functools.cache (ab Python 3.9) ist die einfachere Variante von lru_cache ohne Größenlimit.

Besonders lru_cache zeigt, wie viel ein einziger Dekorator bewirken kann – hier bei der klassischen rekursiven Fibonacci-Funktion:

import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(100))  # blitzschnell dank Cache

Ohne den Cache würde fib(100) praktisch nie fertig werden, weil dieselben Teilergebnisse milliardenfach neu berechnet würden. Mit Cache sind es nur noch 101 echte Aufrufe.

Fazit

Dekoratoren sind kein Hexenwerk: Sie sind Funktionen, die Funktionen entgegennehmen und erweiterte Versionen zurückgeben. Das @-Zeichen ist dabei nur eine elegante Schreibweise für diese Übergabe. Mit *args, **kwargs und functools.wraps baust du robuste Wrapper, und mit einer zusätzlichen Ebene werden sie sogar konfigurierbar. Wenn du das nächste Mal Logging, Zeitmessung oder Caching quer durch dein Projekt brauchst, schreibst du die Logik genau einmal – und dekorierst einfach jede Funktion, die sie braucht. Probier es aus: Nimm den stoppuhr-Dekorator aus diesem Beitrag und miss damit die langsamste Funktion in deinem aktuellen Projekt.