Dekoratoren in Python verstehen
Lerne Dekoratoren in Python: Funktionen erweitern mit Closures, @-Syntax, functools.wraps und praktische Beispiele wie Timer und Logging.
Dekoratoren in Python verstehen
Dekoratoren sind eines der elegantesten Konzepte in Python. Sie erlauben es dir, das Verhalten von Funktionen zu erweitern, ohne deren Code direkt zu aendern. In diesem Tutorial lernst du Schritt fuer Schritt, wie Dekoratoren funktionieren und wie du eigene schreibst.
Was sind Dekoratoren?
Ein Dekorator ist eine Funktion, die eine andere Funktion entgegennimmt und eine erweiterte Version davon zurueckgibt. Stell dir vor, du moechtest bei jeder Funktion messen, wie lange sie braucht. Statt den Messcode in jede Funktion zu schreiben, erstellst du einen Dekorator, der das automatisch erledigt.
# Ohne Dekorator: Wiederholter Code
import time
def langsame_funktion():
start = time.time()
time.sleep(1)
print("Fertig!")
ende = time.time()
print(f"Dauer: {ende - start:.2f}s")
def andere_funktion():
start = time.time()
time.sleep(0.5)
print("Auch fertig!")
ende = time.time()
print(f"Dauer: {ende - start:.2f}s")
Das ist viel Wiederholung. Ein Dekorator loest das elegant - aber bevor wir dorthin kommen, muessen wir zwei Voraussetzungen verstehen.
Funktionen als Parameter uebergeben
In Python sind Funktionen First-Class Objects. Du kannst sie wie jeden anderen Wert behandeln und auch als Argument an andere Funktionen uebergeben:
def sage_hallo(name):
return f"Hallo, {name}!"
def sage_tschuess(name):
return f"Tschuess, {name}!"
def fuehre_aus(funktion, name):
"""Nimmt eine Funktion und fuehrt sie aus."""
ergebnis = funktion(name)
print(ergebnis)
fuehre_aus(sage_hallo, "Anna") # Ausgabe: Hallo, Anna!
fuehre_aus(sage_tschuess, "Max") # Ausgabe: Tschuess, Max!
Beachte: Wir uebergeben sage_hallo (ohne Klammern), nicht sage_hallo(). Ohne Klammern uebergeben wir die Funktion selbst, mit Klammern wuerden wir sie aufrufen und das Ergebnis uebergeben.
Inner Functions und Closures (Voraussetzung)
Du kannst Funktionen innerhalb von Funktionen definieren. Diese inneren Funktionen haben Zugriff auf Variablen der aeusseren Funktion - das nennt man Closure:
def aeussere_funktion(nachricht):
"""Eine Funktion, die eine innere Funktion zurueckgibt."""
def innere_funktion():
# Hat Zugriff auf 'nachricht' aus der aeusseren Funktion!
print(f"Nachricht: {nachricht}")
return innere_funktion # Gibt die Funktion zurueck (nicht aufrufen!)
# Die aeussere Funktion gibt eine innere Funktion zurueck
mein_gruss = aeussere_funktion("Hallo, Welt!")
mein_gruss() # Ausgabe: Nachricht: Hallo, Welt!
# Obwohl aeussere_funktion laengst fertig ist,
# "erinnert" sich die innere Funktion an den Wert von 'nachricht'.
# Das ist ein Closure!
Ein weiteres Beispiel fuer Closures:
def erstelle_multiplizierer(faktor):
"""Erstellt eine Funktion, die mit einem bestimmten Faktor multipliziert."""
def multiplizierer(zahl):
return zahl * faktor
return multiplizierer
verdopple = erstelle_multiplizierer(2)
verdreifache = erstelle_multiplizierer(3)
print(verdopple(5)) # 10
print(verdreifache(5)) # 15
Jetzt haben wir alle Bausteine fuer Dekoratoren: Funktionen als Parameter, innere Funktionen und Closures.
Einen einfachen Dekorator bauen
Ein Dekorator ist eine Funktion, die:
- Eine Funktion als Parameter entgegennimmt
- Eine neue (innere) Funktion definiert, die die originale erweitert
- Diese neue Funktion zurueckgibt
def mein_dekorator(funktion):
"""Ein einfacher Dekorator, der Vor- und Nachnachrichten hinzufuegt."""
def wrapper():
print("--- Vor dem Funktionsaufruf ---")
funktion() # Die originale Funktion ausfuehren
print("--- Nach dem Funktionsaufruf ---")
return wrapper
def sage_hallo():
print("Hallo, Welt!")
# Dekorator manuell anwenden
dekorierte_funktion = mein_dekorator(sage_hallo)
dekorierte_funktion()
# Ausgabe:
# --- Vor dem Funktionsaufruf ---
# Hallo, Welt!
# --- Nach dem Funktionsaufruf ---
Das funktioniert, aber die manuelle Anwendung ist umstaendlich. Hier kommt die @-Syntax ins Spiel.
Die @-Syntax (Syntactic Sugar)
Python bietet eine elegante Kurzschreibweise fuer Dekoratoren mit dem @-Symbol:
def mein_dekorator(funktion):
def wrapper():
print("--- Vor dem Funktionsaufruf ---")
funktion()
print("--- Nach dem Funktionsaufruf ---")
return wrapper
@mein_dekorator
def sage_hallo():
print("Hallo, Welt!")
# Das ist identisch mit: sage_hallo = mein_dekorator(sage_hallo)
sage_hallo()
# Ausgabe:
# --- Vor dem Funktionsaufruf ---
# Hallo, Welt!
# --- Nach dem Funktionsaufruf ---
Der Dekorator mit @ wird direkt ueber der Funktionsdefinition geschrieben. Es ist reiner Syntactic Sugar - also eine Kurzschreibweise fuer etwas, das wir auch anders ausdruecken koennten.
Dekorator mit Argumenten und Rueckgabewerten
Unser bisheriger Dekorator funktioniert nur mit Funktionen ohne Parameter und ohne Rueckgabewert. Machen wir ihn universell:
def mein_dekorator(funktion):
def wrapper(*args, **kwargs):
print(f"Aufruf von '{funktion.__name__}' mit args={args}, kwargs={kwargs}")
ergebnis = funktion(*args, **kwargs)
print(f"Ergebnis: {ergebnis}")
return ergebnis
return wrapper
@mein_dekorator
def addiere(a, b):
return a + b
@mein_dekorator
def begruessung(name, gruss="Hallo"):
return f"{gruss}, {name}!"
print(addiere(3, 5))
# Ausgabe:
# Aufruf von 'addiere' mit args=(3, 5), kwargs={}
# Ergebnis: 8
# 8
print(begruessung("Anna", gruss="Hi"))
# Ausgabe:
# Aufruf von 'begruessung' mit args=('Anna',), kwargs={'gruss': 'Hi'}
# Ergebnis: Hi, Anna!
# Hi, Anna!
Durch *args und **kwargs in der wrapper-Funktion akzeptiert der Dekorator jede beliebige Funktionssignatur.
functools.wraps - Metadaten erhalten
Es gibt ein Problem mit unserem Dekorator: Die dekorierte Funktion “verliert” ihre Identitaet:
def mein_dekorator(funktion):
def wrapper(*args, **kwargs):
return funktion(*args, **kwargs)
return wrapper
@mein_dekorator
def addiere(a, b):
"""Addiert zwei Zahlen."""
return a + b
print(addiere.__name__) # 'wrapper' statt 'addiere' !
print(addiere.__doc__) # None statt 'Addiert zwei Zahlen.' !
Die Loesung ist functools.wraps, ein Dekorator fuer den Wrapper, der die Metadaten der originalen Funktion kopiert:
from functools import wraps
def mein_dekorator(funktion):
@wraps(funktion) # Kopiert Name, Docstring etc. von 'funktion'
def wrapper(*args, **kwargs):
return funktion(*args, **kwargs)
return wrapper
@mein_dekorator
def addiere(a, b):
"""Addiert zwei Zahlen."""
return a + b
print(addiere.__name__) # 'addiere' - korrekt!
print(addiere.__doc__) # 'Addiert zwei Zahlen.' - korrekt!
Verwende @wraps immer in deinen Dekoratoren! Es ist eine Best Practice und verhindert schwer aufzufindende Probleme.
Dekoratoren mit Argumenten
Manchmal moechtest du dem Dekorator selbst Argumente uebergeben. Dafuer brauchst du eine weitere Verschachtelungsebene - eine Funktion, die den Dekorator erstellt:
from functools import wraps
def wiederhole(anzahl):
"""Dekorator-Fabrik: Erstellt einen Dekorator, der die Funktion n-mal ausfuehrt."""
def dekorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
ergebnisse = []
for i in range(anzahl):
ergebnis = funktion(*args, **kwargs)
ergebnisse.append(ergebnis)
return ergebnisse
return wrapper
return dekorator
@wiederhole(3)
def wuerfle():
import random
return random.randint(1, 6)
print(wuerfle()) # z.B. [4, 2, 6]
Die Struktur ist:
wiederhole(anzahl)- die aeussere Funktion nimmt die Dekorator-Argumentedekorator(funktion)- der eigentliche Dekorator nimmt die Funktionwrapper(*args, **kwargs)- die erweiterte Funktion
Ein weiteres Beispiel:
from functools import wraps
def nur_wenn(bedingung):
"""Fuehrt die Funktion nur aus, wenn die Bedingung True ist."""
def dekorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
if bedingung:
return funktion(*args, **kwargs)
else:
print(f"'{funktion.__name__}' wurde uebersprungen (Bedingung nicht erfuellt)")
return None
return wrapper
return dekorator
DEBUG = True
@nur_wenn(DEBUG)
def debug_info(nachricht):
print(f"[DEBUG] {nachricht}")
debug_info("Variable x = 42")
# Ausgabe: [DEBUG] Variable x = 42
Praxis: Timer-Dekorator
Ein Timer-Dekorator ist eines der nuetzlichsten Werkzeuge in der Praxis:
import time
from functools import wraps
def timer(funktion):
"""Misst die Ausfuehrungszeit einer Funktion."""
@wraps(funktion)
def wrapper(*args, **kwargs):
start = time.perf_counter()
ergebnis = funktion(*args, **kwargs)
ende = time.perf_counter()
dauer = ende - start
if dauer < 0.001:
print(f"[TIMER] '{funktion.__name__}' brauchte {dauer*1000000:.1f} Mikrosekunden")
elif dauer < 1:
print(f"[TIMER] '{funktion.__name__}' brauchte {dauer*1000:.2f} Millisekunden")
else:
print(f"[TIMER] '{funktion.__name__}' brauchte {dauer:.4f} Sekunden")
return ergebnis
return wrapper
@timer
def langsame_berechnung(n):
"""Berechnet die Summe von 1 bis n."""
return sum(range(1, n + 1))
@timer
def finde_primzahlen(grenze):
"""Findet alle Primzahlen bis zur Grenze (Sieb des Eratosthenes)."""
sieb = [True] * (grenze + 1)
sieb[0] = sieb[1] = False
for i in range(2, int(grenze**0.5) + 1):
if sieb[i]:
for j in range(i*i, grenze + 1, i):
sieb[j] = False
return [i for i in range(grenze + 1) if sieb[i]]
# Testen
ergebnis = langsame_berechnung(1000000)
print(f"Summe: {ergebnis}")
primzahlen = finde_primzahlen(100000)
print(f"Anzahl Primzahlen: {len(primzahlen)}")
Erweiterter Timer mit Argumenten
import time
from functools import wraps
def timer_erweitert(einheit="auto", ausgabe=True):
"""Timer-Dekorator mit konfigurierbarer Einheit und Ausgabe."""
def dekorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
start = time.perf_counter()
ergebnis = funktion(*args, **kwargs)
dauer = time.perf_counter() - start
if einheit == "s":
dauer_str = f"{dauer:.4f} Sekunden"
elif einheit == "ms":
dauer_str = f"{dauer*1000:.2f} Millisekunden"
elif einheit == "auto":
if dauer < 0.001:
dauer_str = f"{dauer*1000000:.1f} us"
elif dauer < 1:
dauer_str = f"{dauer*1000:.2f} ms"
else:
dauer_str = f"{dauer:.4f} s"
if ausgabe:
print(f"[TIMER] {funktion.__name__}: {dauer_str}")
wrapper.letzte_dauer = dauer
return ergebnis
wrapper.letzte_dauer = 0
return wrapper
return dekorator
@timer_erweitert(einheit="ms")
def schnelle_funktion():
return sum(range(10000))
ergebnis = schnelle_funktion()
print(f"Ergebnis: {ergebnis}")
print(f"Gespeicherte Dauer: {schnelle_funktion.letzte_dauer*1000:.2f} ms")
Praxis: Logging-Dekorator
import logging
from functools import wraps
from datetime import datetime
# Logger konfigurieren
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
def log_aufruf(level="INFO"):
"""Dekorator, der jeden Funktionsaufruf protokolliert."""
def dekorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
log_func = getattr(logger, level.lower())
# Argumente formatieren
args_str = ", ".join(repr(a) for a in args)
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
alle_args = ", ".join(filter(None, [args_str, kwargs_str]))
log_func(f"AUFRUF: {funktion.__name__}({alle_args})")
try:
ergebnis = funktion(*args, **kwargs)
log_func(f"ERGEBNIS: {funktion.__name__} -> {ergebnis!r}")
return ergebnis
except Exception as e:
logger.error(f"FEHLER in {funktion.__name__}: {type(e).__name__}: {e}")
raise
return wrapper
return dekorator
@log_aufruf(level="DEBUG")
def dividiere(a, b):
"""Dividiert a durch b."""
return a / b
@log_aufruf(level="INFO")
def suche_benutzer(name, aktiv=True):
"""Sucht einen Benutzer."""
# Simulierte Suche
return {"name": name, "aktiv": aktiv}
# Testen
print(dividiere(10, 3))
print(suche_benutzer("Anna", aktiv=True))
try:
dividiere(10, 0)
except ZeroDivisionError:
print("Division durch Null abgefangen!")
Eingebaute Dekoratoren
Python bringt einige nuetzliche Dekoratoren mit:
@property - Getter und Setter
class Temperatur:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Temperatur in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, wert):
if wert < -273.15:
raise ValueError("Temperatur kann nicht unter -273.15 Grad C liegen!")
self._celsius = wert
@property
def fahrenheit(self):
"""Temperatur in Fahrenheit (nur lesen)."""
return self._celsius * 9/5 + 32
temp = Temperatur(25)
print(temp.celsius) # 25 (verwendet den @property Getter)
print(temp.fahrenheit) # 77.0
temp.celsius = 100 # Verwendet den @celsius.setter
print(temp.fahrenheit) # 212.0
# temp.fahrenheit = 50 # AttributeError! Kein Setter definiert
@staticmethod - Statische Methoden
class MathUtils:
@staticmethod
def ist_gerade(zahl):
"""Prueft, ob eine Zahl gerade ist. Braucht kein 'self'."""
return zahl % 2 == 0
@staticmethod
def fakultaet(n):
"""Berechnet die Fakultaet von n."""
if n <= 1:
return 1
return n * MathUtils.fakultaet(n - 1)
# Kann ohne Instanz aufgerufen werden
print(MathUtils.ist_gerade(4)) # True
print(MathUtils.fakultaet(5)) # 120
@classmethod - Klassenmethoden
class Datum:
def __init__(self, tag, monat, jahr):
self.tag = tag
self.monat = monat
self.jahr = jahr
@classmethod
def von_string(cls, datum_string):
"""Erstellt ein Datum aus einem String wie '25.12.2024'."""
tag, monat, jahr = datum_string.split(".")
return cls(int(tag), int(monat), int(jahr))
@classmethod
def heute(cls):
"""Erstellt ein Datum mit dem heutigen Tag."""
from datetime import date
h = date.today()
return cls(h.day, h.month, h.year)
def __repr__(self):
return f"Datum({self.tag:02d}.{self.monat:02d}.{self.jahr})"
# Verschiedene Wege ein Datum zu erstellen
d1 = Datum(25, 12, 2024)
d2 = Datum.von_string("25.12.2024")
d3 = Datum.heute()
print(d1) # Datum(25.12.2024)
print(d2) # Datum(25.12.2024)
print(d3) # Datum(10.02.2026) oder aktuelles Datum
Mehrere Dekoratoren stapeln
Du kannst mehrere Dekoratoren auf eine Funktion anwenden. Sie werden von unten nach oben angewendet:
from functools import wraps
def fett(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
return f"<b>{funktion(*args, **kwargs)}</b>"
return wrapper
def kursiv(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
return f"<i>{funktion(*args, **kwargs)}</i>"
return wrapper
def unterstrichen(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
return f"<u>{funktion(*args, **kwargs)}</u>"
return wrapper
@fett
@kursiv
@unterstrichen
def begruessung(name):
return f"Hallo, {name}!"
print(begruessung("Anna"))
# Ausgabe: <b><i><u>Hallo, Anna!</u></i></b>
# Das ist identisch mit:
# begruessung = fett(kursiv(unterstrichen(begruessung)))
Die Reihenfolge ist wichtig! Die Dekoratoren werden von innen nach aussen angewendet:
- Zuerst
unterstrichen(am naechsten zur Funktion) - Dann
kursiv - Zum Schluss
fett(am weitesten von der Funktion entfernt)
Ein praxisnahes Beispiel mit gestapelten Dekoratoren:
import time
from functools import wraps
def timer(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
start = time.perf_counter()
ergebnis = funktion(*args, **kwargs)
dauer = time.perf_counter() - start
print(f"[TIMER] {funktion.__name__}: {dauer:.4f}s")
return ergebnis
return wrapper
def cache(funktion):
"""Einfacher Cache-Dekorator (Memoization)."""
gespeichert = {}
@wraps(funktion)
def wrapper(*args):
if args not in gespeichert:
gespeichert[args] = funktion(*args)
else:
print(f"[CACHE] Treffer fuer {funktion.__name__}{args}")
return gespeichert[args]
wrapper.cache_leeren = lambda: gespeichert.clear()
return wrapper
def validiere_positiv(funktion):
"""Stellt sicher, dass alle Argumente positiv sind."""
@wraps(funktion)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"Negative Werte nicht erlaubt: {arg}")
return funktion(*args, **kwargs)
return wrapper
@timer
@cache
@validiere_positiv
def fibonacci(n):
"""Berechnet die n-te Fibonacci-Zahl (rekursiv)."""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Erster Aufruf - berechnet und cached
print(f"fibonacci(10) = {fibonacci(10)}")
# Zweiter Aufruf - aus dem Cache
print(f"fibonacci(10) = {fibonacci(10)}")
Weiterfuehrende Beispiele
Retry-Dekorator (Wiederholungsversuche)
import time
from functools import wraps
def retry(max_versuche=3, wartezeit=1, fehlertypen=(Exception,)):
"""Wiederholt eine Funktion bei Fehlern."""
def dekorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
letzter_fehler = None
for versuch in range(1, max_versuche + 1):
try:
return funktion(*args, **kwargs)
except fehlertypen as e:
letzter_fehler = e
if versuch < max_versuche:
print(f"Versuch {versuch}/{max_versuche} fehlgeschlagen: {e}")
print(f"Warte {wartezeit}s vor erneutem Versuch...")
time.sleep(wartezeit)
else:
print(f"Alle {max_versuche} Versuche fehlgeschlagen!")
raise letzter_fehler
return wrapper
return dekorator
import random
@retry(max_versuche=5, wartezeit=0.1)
def instabiler_dienst():
"""Simuliert einen unzuverlaessigen Dienst."""
if random.random() < 0.7: # 70% Fehlerrate
raise ConnectionError("Verbindung fehlgeschlagen!")
return "Erfolg!"
try:
ergebnis = instabiler_dienst()
print(f"Ergebnis: {ergebnis}")
except ConnectionError as e:
print(f"Endgueltig fehlgeschlagen: {e}")
Berechtigungs-Dekorator
from functools import wraps
def erfordert_berechtigung(berechtigung):
"""Prueft, ob der Benutzer die erforderliche Berechtigung hat."""
def dekorator(funktion):
@wraps(funktion)
def wrapper(benutzer, *args, **kwargs):
if berechtigung not in benutzer.get("berechtigungen", []):
raise PermissionError(
f"Berechtigung '{berechtigung}' erforderlich! "
f"{benutzer['name']} hat: {benutzer.get('berechtigungen', [])}"
)
return funktion(benutzer, *args, **kwargs)
return wrapper
return dekorator
@erfordert_berechtigung("admin")
def loesche_benutzer(benutzer, ziel_id):
print(f"{benutzer['name']} loescht Benutzer {ziel_id}")
@erfordert_berechtigung("lesen")
def zeige_daten(benutzer):
print(f"{benutzer['name']} sieht die Daten")
# Testen
admin = {"name": "Anna", "berechtigungen": ["admin", "lesen", "schreiben"]}
gast = {"name": "Max", "berechtigungen": ["lesen"]}
zeige_daten(admin) # OK
zeige_daten(gast) # OK
loesche_benutzer(admin, 42) # OK
try:
loesche_benutzer(gast, 42) # PermissionError!
except PermissionError as e:
print(f"Zugriff verweigert: {e}")
Eingabe-Validierungs-Dekorator
from functools import wraps
def validiere_typen(**erwartete_typen):
"""Prueft die Typen der Argumente zur Laufzeit."""
def dekorator(funktion):
@wraps(funktion)
def wrapper(*args, **kwargs):
# Parameternamen der Funktion ermitteln
import inspect
sig = inspect.signature(funktion)
param_namen = list(sig.parameters.keys())
# Positionale Argumente pruefen
for i, (name, wert) in enumerate(zip(param_namen, args)):
if name in erwartete_typen:
if not isinstance(wert, erwartete_typen[name]):
raise TypeError(
f"Parameter '{name}' erwartet {erwartete_typen[name].__name__}, "
f"erhalten {type(wert).__name__}"
)
# Keyword-Argumente pruefen
for name, wert in kwargs.items():
if name in erwartete_typen:
if not isinstance(wert, erwartete_typen[name]):
raise TypeError(
f"Parameter '{name}' erwartet {erwartete_typen[name].__name__}, "
f"erhalten {type(wert).__name__}"
)
return funktion(*args, **kwargs)
return wrapper
return dekorator
@validiere_typen(name=str, alter=int)
def erstelle_profil(name, alter):
return {"name": name, "alter": alter}
print(erstelle_profil("Anna", 28)) # OK
print(erstelle_profil(name="Max", alter=30)) # OK
try:
erstelle_profil("Anna", "achtundzwanzig") # TypeError!
except TypeError as e:
print(f"Typfehler: {e}")
Uebungen
Uebung 1: Einfacher Dekorator
Schreibe einen Dekorator grossbuchstaben, der den Rueckgabewert einer Funktion in Grossbuchstaben umwandelt:
from functools import wraps
def grossbuchstaben(funktion):
"""Wandelt den Rueckgabewert in Grossbuchstaben um."""
@wraps(funktion)
def wrapper(*args, **kwargs):
ergebnis = funktion(*args, **kwargs)
if isinstance(ergebnis, str):
return ergebnis.upper()
return ergebnis
return wrapper
@grossbuchstaben
def begruessung(name):
return f"Hallo, {name}!"
print(begruessung("Anna")) # HALLO, ANNA!
Uebung 2: Zaehler-Dekorator
Schreibe einen Dekorator, der zaehlt, wie oft eine Funktion aufgerufen wurde:
from functools import wraps
def zaehle_aufrufe(funktion):
"""Zaehlt, wie oft eine Funktion aufgerufen wird."""
@wraps(funktion)
def wrapper(*args, **kwargs):
wrapper.aufrufe += 1
print(f"[{funktion.__name__}] Aufruf Nr. {wrapper.aufrufe}")
return funktion(*args, **kwargs)
wrapper.aufrufe = 0
return wrapper
@zaehle_aufrufe
def sage_hallo(name):
return f"Hallo, {name}!"
sage_hallo("Anna") # [sage_hallo] Aufruf Nr. 1
sage_hallo("Max") # [sage_hallo] Aufruf Nr. 2
sage_hallo("Lisa") # [sage_hallo] Aufruf Nr. 3
print(f"Gesamtaufrufe: {sage_hallo.aufrufe}") # 3
Uebung 3: Cache-Dekorator mit Ablaufzeit
import time
from functools import wraps
def cache_mit_ablauf(sekunden=60):
"""Cache-Dekorator, dessen Eintraege nach einer Zeit ablaufen."""
def dekorator(funktion):
gespeichert = {}
@wraps(funktion)
def wrapper(*args):
jetzt = time.time()
if args in gespeichert:
ergebnis, zeitstempel = gespeichert[args]
if jetzt - zeitstempel < sekunden:
print(f"[CACHE] Treffer fuer {args}")
return ergebnis
else:
print(f"[CACHE] Abgelaufen fuer {args}")
ergebnis = funktion(*args)
gespeichert[args] = (ergebnis, jetzt)
return ergebnis
wrapper.cache_info = lambda: {
"eintraege": len(gespeichert),
"schluessel": list(gespeichert.keys())
}
wrapper.cache_leeren = lambda: gespeichert.clear()
return wrapper
return dekorator
@cache_mit_ablauf(sekunden=5)
def aufwendige_berechnung(n):
"""Simuliert eine aufwendige Berechnung."""
time.sleep(0.1) # Simulierte Wartezeit
return n ** 2
# Erster Aufruf - berechnet
print(aufwendige_berechnung(5)) # 25
# Zweiter Aufruf - aus Cache
print(aufwendige_berechnung(5)) # [CACHE] Treffer
# Cache-Info anzeigen
print(aufwendige_berechnung.cache_info())
Uebung 4: Dekorator-Klasse
Dekoratoren koennen auch als Klassen implementiert werden:
from functools import wraps
class Verfolge:
"""Dekorator-Klasse, die Aufrufe einer Funktion verfolgt."""
def __init__(self, funktion):
wraps(funktion)(self)
self.funktion = funktion
self.historie = []
def __call__(self, *args, **kwargs):
import time
start = time.perf_counter()
try:
ergebnis = self.funktion(*args, **kwargs)
dauer = time.perf_counter() - start
self.historie.append({
"args": args,
"kwargs": kwargs,
"ergebnis": ergebnis,
"dauer": dauer,
"fehler": None
})
return ergebnis
except Exception as e:
dauer = time.perf_counter() - start
self.historie.append({
"args": args,
"kwargs": kwargs,
"ergebnis": None,
"dauer": dauer,
"fehler": str(e)
})
raise
def zeige_historie(self):
for i, eintrag in enumerate(self.historie, 1):
status = "OK" if eintrag["fehler"] is None else f"FEHLER: {eintrag['fehler']}"
print(f" {i}. args={eintrag['args']} -> {status} ({eintrag['dauer']*1000:.2f}ms)")
@Verfolge
def addiere(a, b):
return a + b
addiere(1, 2)
addiere(10, 20)
addiere(100, 200)
print("Aufruf-Historie:")
addiere.zeige_historie()
# 1. args=(1, 2) -> OK (0.01ms)
# 2. args=(10, 20) -> OK (0.01ms)
# 3. args=(100, 200) -> OK (0.00ms)
Pro-Tipp: Dekoratoren in der Praxis
Dekoratoren sind eines der maechtigs ten Werkzeuge in Python, aber uebertreibe es nicht. Hier sind Faustregeln:
- Verwende
@wrapsimmer - es bewahrt den Namen, Docstring und andere Metadaten der originalen Funktion - Halte Dekoratoren einfach - ein Dekorator sollte eine klar definierte Aufgabe haben
- Vermeide tiefe Verschachtelung - wenn du mehr als 3 Dekoratoren stapelst, wird der Code schwer nachvollziehbar
- Dokumentiere deine Dekoratoren - ein guter Docstring erklaert, was der Dekorator tut und welche Nebeneffekte er hat
- Nutze eingebaute Dekoratoren - bevor du einen eigenen Dekorator schreibst, pruefe ob Python schon einen mitbringt (z.B.
functools.lru_cachestatt eigenem Cache)
In Frameworks wie Flask oder Django wirst du Dekoratoren staendig begegnen (@app.route, @login_required). Das Verstaendnis, wie sie funktionieren, macht dich zu einem besseren Python-Entwickler.