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:
# 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: 60Dieser Ansatz hat mehrere Probleme:
- Code-Duplizierung: Die Logging-Zeilen werden in jeder Funktion wiederholt
- Vermischte Zuständigkeiten: Logging-Code ist mit Geschäftslogik vermischt
- Schwer zu warten: Wenn Sie das Logging-Format ändern möchten, müssen Sie jede Funktion aktualisieren
- Leicht zu vergessen: Neue Funktionen enthalten möglicherweise kein Logging
Dekoratoren lösen diese Probleme, indem Sie das Logging-Verhalten von Ihren Kernfunktionen trennen:
# 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: 60Der 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:
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:
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:
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: 50Die 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:
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: greetingVerfolgen wir, was passiert:
simple_wrappererhältsay_helloalsoriginal_func- Es erstellt eine neue Funktion
wrapper, die:- „Before calling the function“ ausgibt
original_func()aufruft (das istsay_hello)- „After calling the function“ ausgibt
- das Ergebnis zurückgibt
simple_wrappergibt die Funktionwrapperzurück- Wenn wir
wrapped_hello()aufrufen, rufen wir tatsächlichwrapperauf, die die ursprünglichesay_helloinnen 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:
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:
*argssammelt alle Positionsargumente in einem Tuple**kwargssammelt 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:
greet = flexible_wrapper(greet)verwenden wir die @-Syntax:
@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:
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.0Die Syntax @log_call ist exakt gleichbedeutend mit:
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:
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: 30Wenn Dekoratoren gestapelt werden, werden sie von unten nach oben angewendet (derjenige, der der Funktion am nächsten steht, zuerst):
@timer # Als zweites angewendet (äußerste Schicht)
@log_call # Als erstes angewendet (am nächsten an der Funktion)
def process_data(items):
passDas entspricht:
process_data = timer(log_call(process_data))Anwendungsreihenfolge (unten nach oben):
@log_callumhüllt zuerst die Originalfunktion@timerumhüllt das Ergebnis (also die bereits umhüllte Funktion)
Ausführungsreihenfolge (oben nach unten, äußerste nach innerste):
timer-Wrapper startet (äußerster, wird zuerst ausgeführt)log_call-Wrapper startet (innerer Wrapper)- Originalfunktion wird ausgeführt
log_call-Wrapper endettimer-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:
Ausführungsablauf:
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:
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 IDDieser Dekorator:
- Fügt allen Log-Nachrichten Zeitstempel hinzu
- Protokolliert sowohl erfolgreiche Abschlüsse als auch Exceptions
- Wirft Exceptions nach dem Loggen erneut (mit
raiseohne 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:
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 digitsDieser 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:
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 -10Dieser 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:
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:
@log_callauswerten → Ergebnis:log_callselbst (das Funktionsobjekt)- Auf
greetanwenden:greet = log_call(greet)
B) Dekorator-Factory:
Basierend auf diesem Beispiel:
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:
@repeat(3)auswerten → Ergebnis:repeat(3)wird aufgerufen und gibt eine Dekoratorfunktion zurück- Diesen Dekorator auf
greetanwenden: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:
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 decoratorEbene 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:
# Was Sie schreiben:
@repeat(3)
def greet(name):
print(f"Hello, {name}!")Schritt 1: Python wertet repeat(3) aus
decorator = repeat(3) # Factory gibt einen Dekorator zurück (times=3 wird eingefangen)Schritt 2: Python wendet den Dekorator auf greet an
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
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:
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:
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.0Beachten 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:
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 0Wann 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.