Python & AI Tutorials Logo
Python Programmierung

38. Dekoratoren: Verhalten zu Funktionen hinzufügen

Dekoratoren (decorators) gehören zu den leistungsfähigsten Sprachmitteln in Python, um sauberen, wiederverwendbaren Code zu schreiben. Sie ermöglichen es Ihnen, das Verhalten von Funktionen (functions) zu verändern oder zu erweitern, ohne ihren eigentlichen Code zu ändern. In diesem Kapitel bauen wir auf Ihrem Verständnis von First-Class-Funktionen (first-class functions) und Closures (closures) aus Kapitel 23 auf, um zu verstehen, wie Dekoratoren funktionieren und wie Sie sie effektiv einsetzen können.

38.1) Was Dekoratoren sind und warum sie nützlich sind

Ein Dekorator (decorator) ist eine Funktion (function), die eine andere Funktion als Eingabe nimmt und eine modifizierte Version dieser Funktion zurückgibt. Das ist möglich, weil Funktionen in Python, wie wir in Kapitel 23 gelernt haben, First-Class-Objekte (first-class objects) sind – sie können als Argumente übergeben und von anderen Funktionen zurückgegeben werden. Dekoratoren ermöglichen es Ihnen, bestehende Funktionen mit zusätzlichem Verhalten zu umhüllen, sodass Sie gängige Funktionalität wie Logging, Timing, Validierung oder Zugriffskontrolle hinzufügen können, ohne Ihre Kernlogik zu überladen.

Warum Dekoratoren wichtig sind

Stellen Sie sich vor, Sie haben mehrere Funktionen in Ihrem Programm, und Sie möchten protokollieren, wann jede einzelne aufgerufen wird. Ohne Dekoratoren könnten Sie so etwas schreiben:

python
# Ohne Dekoratoren – doppelter Logging-Code
def calculate_total(prices):
    print("Calling calculate_total")
    result = sum(prices)
    print(f"calculate_total returned: {result}")
    return result
 
def find_average(numbers):
    print("Calling find_average")
    result = sum(numbers) / len(numbers)
    print(f"find_average returned: {result}")
    return result
 
def process_order(order_id):
    print("Calling process_order")
    result = f"Order {order_id} processed"
    print(f"process_order returned: {result}")
    return result
 
# Die Funktionen verwenden
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60

Dieser Ansatz hat mehrere Probleme:

  1. Code-Duplizierung: Die Logging-Zeilen werden in jeder Funktion wiederholt
  2. Vermischte Zuständigkeiten: Logging-Code ist mit Geschäftslogik vermischt
  3. Schwer zu warten: Wenn Sie das Logging-Format ändern möchten, müssen Sie jede Funktion aktualisieren
  4. Leicht zu vergessen: Neue Funktionen enthalten möglicherweise kein Logging

Dekoratoren lösen diese Probleme, indem Sie das Logging-Verhalten von Ihren Kernfunktionen trennen:

python
# Mit Dekoratoren – sauber und wartbar
# (Wie man @log_calls erstellt, lernen wir in diesem Kapitel)
 
@log_calls
def calculate_total(prices):
    return sum(prices)
 
@log_calls
def find_average(numbers):
    return sum(numbers) / len(numbers)
 
@log_calls
def process_order(order_id):
    return f"Order {order_id} processed"
 
# Die Verwendung der Funktionen erzeugt die gleiche Ausgabe
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60

Der Unterschied? Das Logging-Verhalten ist einmalig im Dekorator @log_calls definiert und wird überall wiederverwendet. Ihre Kernfunktionen bleiben sauber und auf ihren Hauptzweck fokussiert.

Häufige Anwendungsfälle für Dekoratoren

Dekoratoren sind besonders nützlich für:

  • Logging: Aufzeichnen, wann Funktionen aufgerufen werden und was sie zurückgeben
  • Timing: Messen, wie lange Funktionen für die Ausführung brauchen
  • Validierung: Prüfen, ob Funktionsargumente bestimmte Anforderungen erfüllen
  • Caching: Speichern von Ergebnissen teurer Funktionsaufrufe zur Wiederverwendung
  • Zugriffskontrolle: Prüfen von Berechtigungen, bevor die Funktion ausgeführt werden darf
  • Wiederholungslogik (Retry-Logik): Fehlgeschlagene Operationen automatisch erneut versuchen
  • Typprüfung: Argument- und Rückgabetypen validieren

Der entscheidende Vorteil ist, dass Sie den Dekorator einmal schreiben und ihn mit einer einzigen Codezeile auf viele Funktionen anwenden können.

38.2) Funktionen als Objekte: Die Grundlage von Dekoratoren

Bevor wir Dekoratoren verstehen können, müssen wir das Konzept wiederholen und vertiefen, dass Funktionen in Python First-Class-Objekte (first-class objects) sind. Wie wir in Kapitel 23 gelernt haben, bedeutet das, dass Funktionen Variablen zugewiesen, als Argumente übergeben und von anderen Funktionen zurückgegeben werden können.

Funktionen können Variablen zugewiesen werden

Wenn Sie eine Funktion definieren, erstellt Python ein Funktionsobjekt und bindet es an einen Namen:

python
def greet(name):
    return f"Hello, {name}!"
 
# Das Funktionsobjekt kann einer anderen Variable zugewiesen werden
say_hello = greet
 
# Beide Namen verweisen auf dasselbe Funktionsobjekt
print(greet("Alice"))      # Output: Hello, Alice!
print(say_hello("Bob"))    # Output: Hello, Bob!

Die Namen greet und say_hello verweisen beide auf dasselbe Funktionsobjekt. Das ist grundlegend dafür, wie Dekoratoren funktionieren.

Funktionen können als Argumente übergeben werden

Sie können Funktionen an andere Funktionen übergeben, genau wie jeden anderen Wert:

python
def apply_twice(func, value):
    """Wendet eine Funktion zweimal auf einen Wert an."""
    result = func(value)
    result = func(result)
    return result
 
def add_five(x):
    return x + 5
 
result = apply_twice(add_five, 10)
print(result)  # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)

Hier erhält apply_twice die Funktion add_five als Argument und ruft sie zweimal auf.

Funktionen können andere Funktionen zurückgeben

Eine Funktion kann eine neue Funktion erstellen und zurückgeben:

python
def make_multiplier(factor):
    """Erstellt eine Funktion, die mit einem bestimmten Faktor multipliziert."""
    def multiply(x):
        return x * factor
    return multiply
 
times_three = make_multiplier(3)
times_five = make_multiplier(5)
 
print(times_three(10))  # Output: 30
print(times_five(10))   # Output: 50

Die Funktion make_multiplier gibt eine neue Funktion zurück, die sich den Wert factor über eine Closure (closure) „merkt“ (wie wir in Kapitel 23 gelernt haben).

Funktionen umhüllen: Das Kernmuster eines Dekorators

Das Dekorator-Pattern kombiniert diese Konzepte: eine Funktion, die eine Funktion als Eingabe nimmt, eine Wrapper-Funktion (wrapper function) erstellt, die Verhalten hinzufügt, und den Wrapper zurückgibt:

python
def simple_wrapper(original_func):
    """Wrappt eine Funktion mit zusätzlichem Verhalten."""
    def wrapper():
        print("Before calling the function")
        result = original_func()
        print("After calling the function")
        return result
    return wrapper
 
def say_hello():
    print("Hello!")
    return "greeting"
 
# Funktion manuell wrappen
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
 
print(f"Returned: {return_value}")
# Output: Returned: greeting

Verfolgen wir, was passiert:

  1. simple_wrapper erhält say_hello als original_func
  2. Es erstellt eine neue Funktion wrapper, die:
    • „Before calling the function“ ausgibt
    • original_func() aufruft (das ist say_hello)
    • „After calling the function“ ausgibt
    • das Ergebnis zurückgibt
  3. simple_wrapper gibt die Funktion wrapper zurück
  4. Wenn wir wrapped_hello() aufrufen, rufen wir tatsächlich wrapper auf, die die ursprüngliche say_hello innen aufruft

Das ist das Kernmuster hinter allen Dekoratoren.

Umgang mit Funktionen mit Argumenten

Der obige Wrapper funktioniert nur mit Funktionen, die keine Argumente annehmen. Damit er mit jeder Funktion funktioniert, brauchen wir *args und **kwargs:

python
def flexible_wrapper(original_func):
    """Wrappt eine Funktion, die beliebige Argumente akzeptieren kann."""
    def wrapper(*args, **kwargs):
        # *args erfasst Positionsargumente
        # **kwargs erfasst Keyword-Argumente
        print("Before calling the function")
        result = original_func(*args, **kwargs)
        print("After calling the function")
        return result
    return wrapper
 
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"
 
# Funktion manuell wrappen
greet = flexible_wrapper(greet)
 
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
 
print(result)
# Output: Hello, Alice!
 
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
 
print(result)
# Output: Hi, Bob!

Wie *args und **kwargs funktionieren:

Wie wir in Kapitel 20 gelernt haben, erlauben *args und **kwargs Funktionen, eine variable Anzahl an Argumenten anzunehmen:

  • *args sammelt alle Positionsargumente in einem Tuple
  • **kwargs sammelt alle Keyword-Argumente in einem Dictionary
  • Wenn wir original_func(*args, **kwargs) aufrufen, entpacken wir sie wieder als Argumente für die Originalfunktion

Dieses Pattern ermöglicht es unserem Wrapper, mit jeder Funktion zu arbeiten – unabhängig davon, wie viele Argumente sie annimmt.

Übergang zu sauberer Syntax

Dieses Pattern ist die Grundlage von Dekoratoren. Die Dekorator-Syntax, die wir als Nächstes lernen, ist nur eine sauberere Art, dieses Pattern anzuwenden. Statt zu schreiben:

python
greet = flexible_wrapper(greet)

verwenden wir die @-Syntax:

python
@flexible_wrapper
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

Beides macht exakt dasselbe – die @-Syntax ist nur syntactic sugar, der den Code sauberer und besser lesbar macht.

38.3) Die @decorator-Syntax: Eine sauberere Schreibweise

function_name = decorator(function_name) zu schreiben funktioniert zwar, ist aber umständlich und wird leicht vergessen. Python stellt die @decorator-Syntax als sauberere Möglichkeit bereit, Dekoratoren anzuwenden.

Verwendung des @-Symbols

Statt eine Funktion manuell zu wrappen, können Sie @decorator_name in die Zeile direkt vor die Funktionsdefinition setzen:

python
def log_call(func):
    """Dekorator, der Funktionsaufrufe protokolliert."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
@log_call
def calculate_total(prices):
    return sum(prices)
 
@log_call
def find_average(numbers):
    return sum(numbers) / len(numbers)
 
# Die dekorierten Funktionen verwenden
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
 
print(f"Total: {total}")
# Output: Total: 60
 
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
 
print(f"Average: {average}")
# Output: Average: 20.0

Die Syntax @log_call ist exakt gleichbedeutend mit:

python
def calculate_total(prices):
    return sum(prices)
 
calculate_total = log_call(calculate_total)

Aber die @-Syntax ist viel sauberer und macht sofort klar, dass die Funktion dekoriert ist.

Mehrere Dekoratoren stapeln

Sie können mehrere Dekoratoren auf dieselbe Funktion anwenden, indem Sie sie stapeln:

python
import time
 
def log_call(func):
    """Dekorator, der Funktionsaufrufe protokolliert."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
def timer(func):
    """Dekorator, der die Ausführungszeit einer Funktion misst."""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start_time
        print(f"{func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper
 
@timer
@log_call
def process_data(items):
    total = sum(items)
    return total * 2
 
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
 
print(f"Final result: {result}")
# Output: Final result: 30

Wenn Dekoratoren gestapelt werden, werden sie von unten nach oben angewendet (derjenige, der der Funktion am nächsten steht, zuerst):

python
@timer          # Als zweites angewendet (äußerste Schicht)
@log_call       # Als erstes angewendet (am nächsten an der Funktion)
def process_data(items):
    pass

Das entspricht:

python
process_data = timer(log_call(process_data))

Anwendungsreihenfolge (unten nach oben):

  1. @log_call umhüllt zuerst die Originalfunktion
  2. @timer umhüllt das Ergebnis (also die bereits umhüllte Funktion)

Ausführungsreihenfolge (oben nach unten, äußerste nach innerste):

  1. timer-Wrapper startet (äußerster, wird zuerst ausgeführt)
  2. log_call-Wrapper startet (innerer Wrapper)
  3. Originalfunktion wird ausgeführt
  4. log_call-Wrapper endet
  5. timer-Wrapper endet (äußerster, endet zuletzt)

Stellen Sie sich Dekoratoren wie mehrere Schichten Geschenkpapier vor: Beim Einpacken kommen die äußeren Schichten zuletzt dazu, beim Auspacken entfernen Sie sie dagegen zuerst.

Dekorator-Anwendung:

Originalfunktion
process_data

Schritt 1: @log_call(unterer Dekorator)

log_call wrappt Original

Schritt 2: @timer(oberer Dekorator)

timer wrappt log_call-Wrapper

Final: timer wrappt log_call wrappt Original

Ausführungsablauf:

process_data aufrufen

1. timer-Wrapper startet
2. log_call-Wrapper startet
3. Originalfunktion wird ausgeführt
4. log_call-Wrapper endet
5. timer-Wrapper endet

Gibt Ergebnis zurück

38.4) Praktische Dekorator-Beispiele (Logging, Timing, Validierung)

Sehen wir uns nun mehrere praktische Dekoratoren an, die Sie in echten Programmen verwenden könnten. Diese Beispiele zeigen typische Muster und demonstrieren, wie Dekoratoren praxisnahe Probleme lösen.

Beispiel 1: Erweiterter Logging-Dekorator

Ein ausgefeilterer Logging-Dekorator, der Zeitstempel enthält und Exceptions behandelt:

python
import time
 
def log_with_timestamp(func):
    """Dekorator, der Funktionsaufrufe mit Zeitstempeln protokolliert."""
    def wrapper(*args, **kwargs):
        timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{timestamp}] Calling {func.__name__}")
        
        try:
            result = func(*args, **kwargs)
            print(f"[{timestamp}] {func.__name__} completed successfully")
            return result
        except Exception as e:
            print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
            raise
    
    return wrapper
 
@log_with_timestamp
def divide(a, b):
    return a / b
 
@log_with_timestamp
def process_user(user_id):
    # Verarbeitung simulieren
    if user_id < 0:
        raise ValueError("User ID must be positive")
    return f"Processed user {user_id}"
 
# Erfolgreiche Ausführung testen
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
 
print(f"Result: {result}")
# Output: Result: 5.0
 
# Erfolgreiche Ausführung mit Validierung testen
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
 
print(user)
# Output: Processed user 42
 
# Exception-Handling testen
try:
    divide(10, 0)
    # Output:
    # [2025-12-31 10:30:45] Calling divide
    # [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
    print("Handled division by zero")
    # Output: Handled division by zero
 
try:
    process_user(-5)
    # Output:
    # [2025-12-31 10:30:45] Calling process_user
    # [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
    print("Handled invalid user ID")
    # Output: Handled invalid user ID

Dieser Dekorator:

  • Fügt allen Log-Nachrichten Zeitstempel hinzu
  • Protokolliert sowohl erfolgreiche Abschlüsse als auch Exceptions
  • Wirft Exceptions nach dem Loggen erneut (mit raise ohne Argument)
  • Verwendet einen try/except-Block, um jede Exception abzufangen und zu protokollieren

Beispiel 2: Dekorator zur Performance-Zeitmessung

Ein Dekorator, der die Ausführungszeit einer Funktion misst und berichtet:

python
import time
 
def measure_time(func):
    """Dekorator, der die Ausführungszeit misst und berichtet."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        
        # Zeit passend formatieren
        if elapsed < 0.001:
            time_str = f"{elapsed * 1000000:.2f} microseconds"
        elif elapsed < 1:
            time_str = f"{elapsed * 1000:.2f} milliseconds"
        else:
            time_str = f"{elapsed:.2f} seconds"
        
        print(f"{func.__name__} executed in {time_str}")
        return result
    
    return wrapper
 
@measure_time
def find_primes(limit):
    """Findet alle Primzahlen bis limit."""
    primes = []
    for num in range(2, limit):
        is_prime = True
        for divisor in range(2, int(num ** 0.5) + 1):
            if num % divisor == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(num)
    return primes
 
@measure_time
def calculate_factorial(n):
    """Berechnet die Fakultät von n."""
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result
 
# Die dekorierten Funktionen testen
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
 
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
 
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
 
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digits

Dieser Dekorator formatiert die Zeitmessung automatisch passend (Mikrosekunden, Millisekunden oder Sekunden), abhängig von der Dauer.

Beispiel 3: Dekorator zur Eingabevalidierung

Ein Dekorator, der Funktionsargumente vor der Ausführung validiert:

python
def validate_positive(func):
    """Dekorator, der sicherstellt, dass alle numerischen Argumente positiv sind."""
    def wrapper(*args, **kwargs):
        # Positionsargumente prüfen
        for i, arg in enumerate(args):
            if isinstance(arg, (int, float)) and arg <= 0:
                raise ValueError(
                    f"Argument {i} to {func.__name__} must be positive, got {arg}"
                )
        
        # Keyword-Argumente prüfen
        for key, value in kwargs.items():
            if isinstance(value, (int, float)) and value <= 0:
                raise ValueError(
                    f"Argument '{key}' to {func.__name__} must be positive, got {value}"
                )
        
        return func(*args, **kwargs)
    
    return wrapper
 
@validate_positive
def calculate_area(width, height):
    """Berechnet die Fläche eines Rechtecks."""
    return width * height
 
@validate_positive
def calculate_discount(price, discount_percent):
    """Berechnet den rabattierten Preis."""
    discount = price * (discount_percent / 100)
    return price - discount
 
# Gültige Eingaben testen
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
 
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
 
# Ungültige Eingaben testen
try:
    calculate_area(-5, 10)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: Argument 0 to calculate_area must be positive, got -5
 
try:
    calculate_discount(100, discount_percent=-10)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10

Dieser Dekorator:

  • Prüft alle numerischen Argumente (sowohl positional als auch keyword)
  • Wirft einen aussagekräftigen Fehler, wenn eines nicht positiv ist
  • Liefert klare Fehlermeldungen, die angeben, welches Argument die Validierung nicht bestanden hat

38.5) (Optional) Dekoratoren mit Argumenten

Bisher waren all unsere Dekoratoren einfache Funktionen, die eine Funktion als Eingabe nehmen. Aber was ist, wenn Sie das Verhalten eines Dekorators konfigurieren möchten? Zum Beispiel möchten Sie vielleicht einen Dekorator mit Wiederholungslogik, bei dem Sie die Anzahl der Versuche angeben können, oder einen Logging-Dekorator, bei dem Sie den Log-Level festlegen können.

Dekoratoren mit Argumenten erfordern eine zusätzliche Ebene an Funktionsverschachtelung. Statt dass ein Dekorator eine Funktion ist, die eine Funktion nimmt, wird er zu einer Funktion, die Argumente nimmt und einen Dekorator zurückgibt.

Das Muster: Dekorator-Factories

Ein Dekorator mit Argumenten ist eigentlich eine Dekorator-Factory (decorator factory) – eine Funktion, die einen Dekorator erstellt und zurückgibt. Der Schlüssel zum Verständnis ist zu wissen, was Python mit dem @-Symbol macht.

Das Schlüsselprinzip: Python wertet @ zuerst aus

Python wertet immer zuerst aus, was nach @ steht, und verwendet dann das Ergebnis, um Ihre Funktion zu dekorieren.

Vergleichen wir:

A) Basis-Dekorator:

Basierend auf diesem Beispiel:

python
def log_call(func):
    """Dekorator, der Funktionsaufrufe protokolliert."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
@log_call
def greet(name):
    return f"Hello, {name}!"

Was Python macht:

  1. @log_call auswerten → Ergebnis: log_call selbst (das Funktionsobjekt)
  2. Auf greet anwenden: greet = log_call(greet)

B) Dekorator-Factory:

Basierend auf diesem Beispiel:

python
def repeat(times):
    """Ebene 1: Factory – erhält Konfiguration"""
    def decorator(func):
        """Ebene 2: Dekorator – erhält die zu dekorierende Funktion"""
        def wrapper(*args, **kwargs):
            """Ebene 3: Wrapper – wird ausgeführt, wenn die dekorierte Funktion aufgerufen wird"""
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator
 
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")
 
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Was Python macht:

  1. @repeat(3) auswerten → Ergebnis: repeat(3) wird aufgerufen und gibt eine Dekoratorfunktion zurück
  2. Diesen Dekorator auf greet anwenden: greet = decorator(greet)

Der Unterschied: @log_call gibt Ihnen die Funktion selbst, aber @repeat(3) ruft eine Funktion (repeat) auf, die einen Dekorator zurückgibt.

Die drei Ebenen verstehen

Eine Dekorator-Factory hat drei verschachtelte Funktionen, jede mit einer spezifischen Rolle:

python
def repeat(times):                      # Ebene 1: Factory
    def decorator(func):                # Ebene 2: Dekorator  
        def wrapper(*args, **kwargs):   # Ebene 3: Wrapper
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

Ebene 1 – Factory (repeat):

  • Nimmt: Konfiguration (times)
  • Gibt zurück: Eine Dekoratorfunktion
  • Wird aufgerufen: Wenn Python @repeat(3) auswertet

Ebene 2 – Dekorator (decorator):

  • Nimmt: Die zu dekorierende Funktion (func)
  • Gibt zurück: Eine Wrapper-Funktion
  • Wird aufgerufen: Unmittelbar nach Ebene 1, als Teil der @-Syntax

Ebene 3 – Wrapper (wrapper):

  • Nimmt: Die Argumente der Funktion beim Aufruf (*args, **kwargs)
  • Gibt zurück: Das Ergebnis
  • Wird aufgerufen: Jedes Mal, wenn Sie die dekorierte Funktion aufrufen

Schritt-für-Schritt-Ausführung

Verfolgen wir, was bei @repeat(3) passiert:

python
# Was Sie schreiben:
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

Schritt 1: Python wertet repeat(3) aus

python
decorator = repeat(3)  # Factory gibt einen Dekorator zurück (times=3 wird eingefangen)

Schritt 2: Python wendet den Dekorator auf greet an

python
def greet(name):
    print(f"Hello, {name}!")
 
greet = decorator(greet)  # Dekorator gibt einen Wrapper zurück (func=greet wird eingefangen)

Hinweis: An diesem Punkt verweist greet nun auf die Wrapper-Funktion. Die ursprüngliche greet ist in func eingefangen.

Schritt 3: Wenn Sie greet("Alice") aufrufen, wird der Wrapper ausgeführt

python
greet("Alice")  # Ruft tatsächlich wrapper("Alice") auf
# wrapper verwendet die eingefangenen 'times' und 'func'

Warum drei Ebenen?

Jede Ebene fängt über Closures (closures) unterschiedliche Informationen ein:

python
def repeat(times):                      # Fängt ein: times
    def decorator(func):                # Fängt ein: func (und merkt sich times)
        def wrapper(*args, **kwargs):   # Fängt ein: times, func und erhält args
            for i in range(times):      # Nutzt eingefangenes 'times'
                result = func(*args, **kwargs)  # Nutzt eingefangenes 'func' und 'args'
            return result
        return wrapper
    return decorator
  • Ebene 1 fängt die Konfiguration (times) ein
  • Ebene 2 fängt die zu dekorierende Funktion (func) ein
  • Ebene 3 erhält die Argumente beim Aufruf (args, kwargs)

Ohne alle drei Ebenen könnten wir keinen konfigurierbaren Dekorator implementieren, der sich sowohl seine Einstellungen als auch die dekorierte Funktion merkt.

Beispiel 1: Ein konfigurierbarer Logging-Dekorator

Hier ist ein praktisches Beispiel für einen Logging-Dekorator, der Konfiguration akzeptiert:

python
def log_with_prefix(prefix="LOG"):
    """Dekorator-Factory, die einen Logging-Dekorator mit einem benutzerdefinierten Präfix erstellt."""
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] Calling {func.__name__}")
            result = func(*args, **kwargs)
            print(f"[{prefix}] {func.__name__} returned: {result}")
            return result
        return wrapper
    return decorator
 
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
    return sum(prices)
 
@log_with_prefix()  # Standardpräfix verwenden
def get_average(numbers):
    return sum(numbers) / len(numbers)
 
# Die dekorierten Funktionen testen
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
 
print(f"Total: {total}")
# Output: Total: 60
 
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
 
print(f"Average: {average}")
# Output: Average: 20.0

Beachten Sie:

  • @log_with_prefix(prefix="INFO") verwendet ein benutzerdefiniertes Präfix
  • @log_with_prefix() verwendet das Standardpräfix „LOG“
  • Sie müssen Klammern angeben, selbst wenn Sie die Standardwerte verwenden

Beispiel 2: Ein Dekorator mit mehreren Argumenten

Hier ist ein Dekorator, der Zahlenbereiche validiert:

python
def validate_range(min_value=None, max_value=None):
    """
    Dekorator-Factory, die validiert, dass numerische Argumente innerhalb eines Bereichs liegen.
    
    Args:
        min_value: Minimal erlaubter Wert (inklusive)
        max_value: Maximal erlaubter Wert (inklusive)
    """
    def decorator(func):
        def wrapper(*args, **kwargs):
            # Alle numerischen Argumente prüfen
            all_args = list(args) + list(kwargs.values())
            
            for arg in all_args:
                if isinstance(arg, (int, float)):
                    if min_value is not None and arg < min_value:
                        raise ValueError(
                            f"{func.__name__} received {arg}, "
                            f"which is below minimum {min_value}"
                        )
                    if max_value is not None and arg > max_value:
                        raise ValueError(
                            f"{func.__name__} received {arg}, "
                            f"which is above maximum {max_value}"
                        )
            
            return func(*args, **kwargs)
        return wrapper
    return decorator
 
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
    """Calculate percentage."""
    return (value / total) * 100
 
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
    """Calculate age from birth year."""
    return current_year - birth_year
 
# Gültige Eingaben testen
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
 
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
 
# Ungültige Eingaben testen
try:
    calculate_percentage(150, 100)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: calculate_percentage received 150, which is above maximum 100
 
try:
    calculate_age(-5, 2025)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: calculate_age received -5, which is below minimum 0

Wann man Dekoratoren mit Argumenten verwendet

Verwenden Sie Dekoratoren mit Argumenten, wenn:

  • Sie das Verhalten des Dekorators konfigurieren müssen
  • derselbe Dekorator in unterschiedlichen Kontexten unterschiedlich arbeiten soll
  • Sie Dekoratoren wiederverwendbarer und flexibler machen möchten

Häufige Beispiele sind:

  • Retry-Dekoratoren mit konfigurierbaren Versuchen und Verzögerungen
  • Logging-Dekoratoren mit konfigurierbaren Log-Levels oder Formaten
  • Validierungs-Dekoratoren mit konfigurierbaren Regeln
  • Caching-Dekoratoren mit konfigurierbaren Cache-Größen oder Ablaufzeiten
  • Rate-Limiting-Dekoratoren mit konfigurierbaren Limits

Ein Hinweis zur Komplexität

Dekoratoren mit Argumenten fügen eine zusätzliche Ebene an Komplexität hinzu. Beim Schreiben:

  • Verwenden Sie klare, beschreibende Parameternamen
  • Stellen Sie sinnvolle Standardwerte bereit
  • Fügen Sie Docstrings hinzu, die die Parameter erklären
  • Überlegen Sie, ob die zusätzliche Flexibilität die Komplexität wert ist

Für einfache Fälle ist ein Dekorator ohne Argumente oft klarer und leichter zu verstehen.


Dekoratoren sind ein mächtiges Werkzeug, um sauberen, wartbaren Python-Code zu schreiben. Sie erlauben es Ihnen, Querschnittsbelange (cross-cutting concerns) (wie Logging, Timing und Validierung) von Ihrer zentralen Geschäftslogik zu trennen, wodurch Ihr Code leichter zu lesen, zu testen und zu ändern ist. Wenn Sie weiter in Python programmieren, werden Sie Dekoratoren in Frameworks und Bibliotheken sehr häufig sehen, und Sie werden viele Gelegenheiten entdecken, Ihre eigenen Dekoratoren zu schreiben, um gängige Probleme elegant zu lösen.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai