Decorators in Python verstehen

Decorators erweitern Funktionen, ohne ihren Code zu verändern. In diesem Beitrag lernst du Schritt für Schritt, wie Decorators in Python funktionieren und wofür du sie nutzt.

Teilen
Decorators in Python verstehen

Vielleicht ist dir schon einmal die Zeile @app.route("/") in einem Flask-Projekt begegnet oder @staticmethod in einer Klasse. Dieses @-Symbol gehört zu den sogenannten Decorators. Sie wirken anfangs wie Magie, sind aber ein logisches und sehr nützliches Konzept. Ein Decorator erweitert eine Funktion um zusätzliches Verhalten, ohne dass du den ursprünglichen Code anfassen musst. In diesem Beitrag bauen wir uns Schritt für Schritt das nötige Verständnis auf.

Funktionen sind Objekte

Bevor wir Decorators verstehen, musst du eine wichtige Eigenschaft von Python kennen: Funktionen sind ganz normale Objekte. Du kannst sie einer Variablen zuweisen, als Argument übergeben oder aus einer anderen Funktion zurückgeben.

def begruessung():
    return "Hallo!"

# Funktion einer Variablen zuweisen
gruss = begruessung
print(gruss())  # Hallo!

Diese Flexibilität ist die Grundlage für alles, was jetzt folgt. Eine Funktion, die eine andere Funktion entgegennimmt oder zurückgibt, nennt man eine Funktion höherer Ordnung.

Eine Funktion in eine andere verpacken

Ein Decorator ist nichts anderes als eine Funktion, die eine andere Funktion umschließt. Schauen wir uns das ohne die @-Syntax an, damit der Mechanismus klar wird:

def mit_protokoll(funktion):
    def wrapper():
        print("Funktion wird aufgerufen ...")
        ergebnis = funktion()
        print("Funktion ist fertig.")
        return ergebnis
    return wrapper

def sage_hallo():
    print("Hallo Welt")

# Manuell dekorieren
sage_hallo = mit_protokoll(sage_hallo)
sage_hallo()

Die innere Funktion wrapper ergänzt das Verhalten, ruft die übergebene Funktion auf und gibt deren Ergebnis zurück. Genau das macht ein Decorator.

Die @-Syntax

Die Zeile sage_hallo = mit_protokoll(sage_hallo) ist etwas umständlich. Python bietet dafür eine elegante Kurzschreibweise mit dem @-Symbol. Beide Varianten sind exakt gleichbedeutend:

def mit_protokoll(funktion):
    def wrapper():
        print("Start")
        funktion()
        print("Ende")
    return wrapper

@mit_protokoll
def sage_hallo():
    print("Hallo Welt")

sage_hallo()
# Start
# Hallo Welt
# Ende

Du schreibst also einfach @mit_protokoll über die zu dekorierende Funktion. Das ist lesbarer und klarer.

Decorators mit Argumenten

Die meisten Funktionen haben Parameter. Damit dein Decorator beliebige Funktionen umschließen kann, verwendest du *args und **kwargs. So werden alle Argumente einfach weitergereicht.

import time

def messe_zeit(funktion):
    def wrapper(*args, **kwargs):
        start = time.time()
        ergebnis = funktion(*args, **kwargs)
        dauer = time.time() - start
        print(f"Dauer: {dauer:.4f} Sekunden")
        return ergebnis
    return wrapper

@messe_zeit
def summiere(n):
    return sum(range(n))

print(summiere(1000000))

Dieser Decorator misst, wie lange eine beliebige Funktion läuft. Das ist ein klassisches und sehr praktisches Beispiel.

functools.wraps nicht vergessen

Wenn du eine Funktion dekorierst, geht standardmäßig ihr Name und ihre Dokumentation verloren, weil sie durch den wrapper ersetzt wird. Mit functools.wraps behebst du das elegant:

import functools

def mein_decorator(funktion):
    @functools.wraps(funktion)
    def wrapper(*args, **kwargs):
        return funktion(*args, **kwargs)
    return wrapper

@mein_decorator
def beispiel():
    """Diese Funktion macht etwas Wichtiges."""
    pass

print(beispiel.__name__)  # beispiel
print(beispiel.__doc__)   # Diese Funktion macht etwas Wichtiges.

So bleibt der ursprüngliche Name erhalten, was beim Debuggen und in der Dokumentation hilft. Verwende functools.wraps deshalb immer in deinen Decorators.

Fazit

Decorators sind kein Hexenwerk, sondern beruhen auf einer einfachen Idee: Eine Funktion wird in eine andere verpackt, um sie um zusätzliches Verhalten zu erweitern. Du hast gesehen, dass Funktionen Objekte sind, wie ein Wrapper aufgebaut ist und wie die @-Syntax das Ganze lesbar macht. Mit *args, **kwargs und functools.wraps schreibst du robuste Decorators. Typische Einsatzgebiete sind Protokollierung, Zeitmessung, Zugriffskontrolle und Caching. Sobald du das Prinzip einmal verstanden hast, wirst du Decorators überall wiedererkennen.