W3docs

Python Decorators

Erfahre, wie Python-Decorators funktionieren: eigene schreiben, Metadaten mit functools.wraps erhalten, Decorators stapeln und reale Anwendungsfälle.

Ein Decorator ist eine Funktion, die eine andere Funktion umschließt, um deren Verhalten zu erweitern oder zu verändern, ohne den Quellcode zu ändern. Decorators gehören zu den leistungsfähigsten und idiomatischsten Merkmalen von Python – sie sind die Grundlage hinter @staticmethod, @classmethod, @property, @functools.lru_cache und vielen gängigen Mustern in Web-Frameworks.

Diese Seite erklärt, wie Decorators funktionieren, wie man sie von Grund auf selbst schreibt, wie man Argumente an Decorators übergibt, wie man sie stapelt und wann welches Muster am sinnvollsten ist.

Wie Decorators funktionieren

Ein Decorator ist schlicht eine Funktion, die eine andere Funktion als Argument entgegennimmt und eine neue Funktion zurückgibt. Python bietet die @-Syntax als Kurzform dafür:

@shout
def greet(name):
    return f"hello, {name}"

Das ist genau gleichbedeutend mit:

def greet(name):
    return f"hello, {name}"

greet = shout(greet)

Die Zeile @shout weist Python an: Nach der Definition von greet wird diese sofort an shout übergeben und der Name greet an das gebunden, was shout zurückgibt. Ab diesem Zeitpunkt läuft jeder Aufruf von greet(...) zunächst durch die Logik von shout.

Den ersten Decorator schreiben

Ein Decorator definiert normalerweise eine innere Wrapper-Funktion, die die ursprüngliche Funktion aufruft und zusätzliches Verhalten darum herum hinzufügt:

def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    return f"hello, {name}"

print(greet("world"))   # HELLO, WORLD
print(greet("python"))  # HELLO, PYTHON

wrapper akzeptiert *args und **kwargs, damit es jede Kombination von Argumenten unverändert an func weiterleitet. Dadurch ist der Decorator mit jeder Funktion kompatibel, unabhängig von ihrer Signatur – eine gute Gewohnheit von Anfang an.

Warum der Wrapper die innere Funktion zurückgeben muss

shout endet mit return wrapper, nicht mit return wrapper(). Das ist beabsichtigt: shout erstellt ein neues aufrufbares Objekt, ruft es aber noch nicht auf. Wenn man versehentlich return wrapper() schreibt, würde der Decorator sofort beim Dekorieren ausgeführt werden und greet wäre an den Rückgabewert von wrapper gebunden – einen String – statt an das aufrufbare Objekt selbst.

Metadaten mit functools.wraps erhalten

Jede Python-Funktion trägt Metadaten: __name__, __doc__, __module__ und mehr. Ohne besondere Vorkehrung ersetzt ein Decorator die ursprüngliche Funktion durch wrapper und verliert all das:

def shout(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    """Say hello to name."""
    return f"hello, {name}"

print(greet.__name__)  # wrapper  — wrong
print(greet.__doc__)   # None     — lost

Abhilfe schafft das Anwenden von @functools.wraps(func) auf den Wrapper. Dabei werden die Metadaten der ursprünglichen Funktion auf wrapper kopiert:

import functools

def shout(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    """Say hello to name."""
    return f"hello, {name}"

print(greet("world"))  # HELLO, WORLD
print(greet.__name__)  # greet
print(greet.__doc__)   # Say hello to name.

Verwende immer @functools.wraps in jedem selbst geschriebenen Decorator. Ohne es sehen Debugging-Tools, Dokumentationsgeneratoren und Test-Frameworks den falschen Funktionsnamen. Die einzige Ausnahme ist, wenn man die ursprüngliche Identität bewusst verbergen möchte.

Praktische Decorator-Beispiele

Logger

Jeden Aufruf einer Funktion mit ihren Argumenten und dem Rückgabewert protokollieren:

import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8

Timer

Messen, wie lange eine Funktion zur Ausführung braucht:

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

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

total = slow_sum(1_000_000)
print(total)  # slow_sum took 0.01xxs  then  499999500000

time.perf_counter() ist hier die richtige Wahl, da es die höchste verfügbare Auflösung für Kurzzeitsmessungen bietet.

Memoization (Cache)

Den Rückgabewert für jeden eindeutigen Satz von Argumenten zwischenspeichern, sodass die Funktion für dieselbe Eingabe nie zweimal berechnet wird:

import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 55
print(fibonacci(30))  # 832040

Für Produktionscode ist das eingebaute @functools.lru_cache oder @functools.cache (Python 3.9+) zu bevorzugen, da diese Sonderfälle, Thread-Sicherheit und Cache-Größenbeschränkungen berücksichtigen. Die oben gezeigte handgefertigte Version ist nützlich, um das Muster zu verstehen.

Zugriffskontrolle

Eine Funktion absichern, sodass sie nur ausgeführt werden kann, wenn eine Bedingung erfüllt ist:

import functools

def require_auth(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_authenticated"):
            raise PermissionError("Authentication required.")
        return func(user, *args, **kwargs)
    return wrapper

@require_auth
def get_dashboard(user):
    return f"Welcome, {user['name']}!"

guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}

try:
    print(get_dashboard(guest))
except PermissionError as e:
    print(e)                      # Authentication required.

print(get_dashboard(admin))       # Welcome, Admin!

Decorators mit Argumenten

Manchmal muss ein Decorator zur Dekorationszeit konfiguriert werden – zum Beispiel um eine Funktion eine variable Anzahl von Malen zu wiederholen. Einfache Decorators können keine zusätzlichen Argumente direkt entgegennehmen, weil Python die Funktion übergibt, nicht die Argumente. Die Lösung ist eine Decorator-Factory: eine Funktion, die die Konfiguration entgegennimmt und einen Decorator zurückgibt:

import functools

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

@repeat(3)
def say(message):
    print(message)

say("hello")
# hello
# hello
# hello

Von außen nach innen gelesen: @repeat(3) ruft zunächst repeat(3) auf, was decorator zurückgibt. Python wendet dann decorator auf say an, was wrapper zurückgibt. Damit zeigt say auf wrapper – dasselbe Muster wie zuvor, nur mit einer zusätzlichen Ebene, um n in den Gültigkeitsbereich zu bringen.

Die Verschachtelung kann zunächst abschreckend wirken. Eine mentale Eselsbrücke: Die äußerste Funktion enthält die Konfiguration, die mittlere Funktion die zu dekorierende Funktion und die innerste Funktion den abzufangenden Aufruf.

Mehrere Decorators stapeln

Man kann mehrere Decorators auf eine einzelne Funktion anwenden, indem man @-Zeilen stapelt. Python wendet sie von unten nach oben an – der Decorator, der dem def am nächsten ist, wird zuerst angewendet:

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<b>" + func(*args, **kwargs) + "</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<i>" + func(*args, **kwargs) + "</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

print(greet("Alice"))  # <b><i>Hello, Alice</i></b>

Entspricht greet = bold(italic(greet)). italic umschließt zuerst greet, dann umschließt bold das Ergebnis. Die Ausgabe zeigt, dass italic näher am reinen String läuft und bold außen umschließt.

Klassenbasierte Decorators

Auch eine Klasse kann ein Decorator sein – jedes Objekt mit einer __call__-Methode ist aufrufbar. Klassenbasierte Decorators sind nützlich, wenn der Decorator selbst über Aufrufe hinweg einen Zustand aufrechterhalten muss:

import functools

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

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call #{self.count} to {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()
say_hello()
print(say_hello.count)  # 2

functools.update_wrapper(self, func) übernimmt dieselbe Aufgabe wie @functools.wraps – es kopiert die Metadaten der ursprünglichen Funktion auf die Instanz. Nach der Dekorierung ist say_hello eine CountCalls-Instanz, sodass say_hello.count ein gewöhnlicher Attributzugriff ist.

Wann eine Klasse einem Funktions-Decorator vorzuziehen ist:

  • Man benötigt persistenten Zustand (count, cache, Flags).
  • Der Decorator hat mehrere Methoden oder Hilfslogik.
  • Das dekorierte Objekt soll als spezifischer Typ introspektierbar sein.

Häufige Fallstricke mit Decorators

Die dekorierte Funktion vergessen aufzurufen

Ein häufiger Anfängerfehler ist, den Wrapper zurückzugeben, aber vergessen, func darin aufzurufen:

def broken(func):
    def wrapper(*args, **kwargs):
        print("before")
        # forgot to call func!
    return wrapper

Die dekorierte Funktion gibt jedes Mal stillschweigend None zurück. Stelle immer sicher, dass wrapper func(*args, **kwargs) aufruft und dessen Ergebnis zurückgibt.

Auf der falschen Ebene dekorieren

Bei parametrisierten Decorators ist es ein häufiger Fehler, den äußeren Aufruf zu vergessen:

# Wrong — 'repeat' receives the function, not a count
@repeat      # should be @repeat(3)
def say(msg):
    print(msg)

Dabei wird say an repeat übergeben, wo n erwartet wird, was beim Aufruf von say zu einem TypeError führt.

Die Reihenfolge von Decorators ist wichtig

Bei gestapelten Decorators verändert die Reihenfolge das Verhalten. @timer über @log_calls auf derselben Funktion misst die Zeit der bereits protokollierten Version, während die umgekehrte Reihenfolge die bereits zeitgesteuerte Version protokolliert. Überlege genau, was jede Schicht sehen soll.

Beziehung zu Closures

Die wrapper-Funktion eines Decorators ist ein Closure – sie erfasst func aus dem umgebenden Gültigkeitsbereich und hält ihn am Leben, auch nachdem die äußere Decorator-Funktion bereits zurückgekehrt ist. Das Verstehen von Closures macht die innere Funktionsweise von Decorators offensichtlich: Das Cell-Objekt, das func enthält, ist genau das, was wrapper ermöglicht, die ursprüngliche Funktion aufzurufen, lange nachdem shout(greet) abgeschlossen wurde.

Zur Syntax von *args und **kwargs, die innerhalb von Wrappern verwendet wird, siehe das entsprechende Kapitel. Zu Lambda-Ausdrücken, die sich gut mit Decorators in höherwertigen Mustern kombinieren lassen, siehe das Lambda-Kapitel.

Kurzübersicht

MusterWann verwenden
Einfacher wrapperVerhalten vor/nach einer Funktion hinzufügen
@functools.wrapsImmer – erhält __name__, __doc__
Decorator-Factory (3 Ebenen)Decorator muss konfiguriert werden
Gestapelte DecoratorsMehrere unabhängige Verhaltensweisen kombinieren
Klassenbasierter DecoratorPersistenter Zustand zwischen Aufrufen nötig
@functools.lru_cacheReine Funktionen memoizen (eingebaut, produktionsreif)

Übungen

Übung
What does @functools.wraps(func) do inside a decorator?
What does @functools.wraps(func) do inside a decorator?
Übung
Given @bold applied above @italic on a function, which decorator is applied first?
Given @bold applied above @italic on a function, which decorator is applied first?
Übung
What is a decorator factory?
What is a decorator factory?
Was this page helpful?