Zum Inhalt springen
Python Fortgeschritten 3 min

Dekoratoren in Python verstehen

Lerne Dekoratoren in Python: Funktionen erweitern mit Closures, @-Syntax, functools.wraps und praktische Beispiele wie Timer und Logging.

Aktualisiert:

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:

  1. Eine Funktion als Parameter entgegennimmt
  2. Eine neue (innere) Funktion definiert, die die originale erweitert
  3. 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:

  1. wiederhole(anzahl) - die aeussere Funktion nimmt die Dekorator-Argumente
  2. dekorator(funktion) - der eigentliche Dekorator nimmt die Funktion
  3. wrapper(*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:

  1. Zuerst unterstrichen (am naechsten zur Funktion)
  2. Dann kursiv
  3. 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 @wraps immer - 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_cache statt 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.

Zurück zum Python Kurs