- 1 Section
- 10 Lessons
- unbegrenzt
- Python Fortgeschritten10
- 1.1Module und Pakete: import, pip, venv
- 1.2Objektorientierung in Python: Klassen, __init__, self
- 1.3Vererbung und Mehrfachvererbung in Python
- 1.4Decorators und Generatoren
- 1.5Fehlerbehandlung vertieft: eigene Exceptions
- 1.6Reguläre Ausdrücke mit re
- 1.7Arbeiten mit APIs: requests, JSON
- 1.8Automatisierung mit os, subprocess, pathlib
- 1.9Unit-Tests in Python: pytest
- 1.10Praxisprojekt: Automatisierungsskript mit Tests
Decorators und Generatoren
Decorators und Generatoren sind zwei Konstrukte, die Python von vielen anderen Sprachen unterscheiden. Beide nutzen das Konzept „Funktionen sind Objekte" aus, um sehr eleganten Code zu ermöglichen. Du bist beiden längst begegnet – jedes Mal, wenn du @property oder eine for-Schleife geschrieben hast.
Diese Lektion erklärt, wie beide funktionieren: ein Decorator ist eine Funktion, die eine andere Funktion verändert oder erweitert. Ein Generator ist eine Funktion, die ihre Werte stückweise produziert – speicher-effizient und lazy. Dazu kommen Context Manager und einige eingebaute Decoratoren.
1) Funktionen als Objekte
Bevor Decorators Sinn ergeben, musst du eine Eigenschaft von Python kennen: Funktionen sind Objekte. Du kannst sie wie jeden anderen Wert in einer Variable speichern, an Funktionen übergeben oder von Funktionen zurückgeben:
def begruessen(name): return f"Hallo {name}" gruss = begruessen # Funktion als Wert print(gruss("Anna")) # Hallo Anna def zweimal(funktion, arg): funktion(arg) funktion(arg) zweimal(print, "Hi") # druckt Hi zweimal
Auch Funktionen in Funktionen sind möglich – diese sehen die Variablen der äußeren Funktion (Closure):
def erzeuge_zaehler(): n = 0 def zaehler(): nonlocal n n += 1 return n return zaehler z = erzeuge_zaehler() print(z(), z(), z()) # 1 2 3
2) Ein einfacher Decorator
Ein Decorator ist eine Funktion, die eine andere Funktion entgegennimmt, sie in einer „Hülle" verpackt und die Hülle zurückgibt. Beispiel: ein Logger, der vor und nach jedem Aufruf etwas ausgibt:
def logger(funktion): def huelle(*args, **kwargs): print(f"Aufruf: {funktion.__name__}") ergebnis = funktion(*args, **kwargs) print(f"Fertig: {funktion.__name__}") return ergebnis return huelle def addiere(a, b): return a + b addiere_mit_log = logger(addiere) print(addiere_mit_log(2, 3)) # Aufruf: addiere # Fertig: addiere # 5
Die Funktion logger nimmt eine Funktion entgegen, definiert eine Hülle huelle, die zusätzlich loggt, und gibt die Hülle zurück. Aufruf von addiere_mit_log(...) ruft tatsächlich huelle(...), die intern funktion(...) aufruft.
3) Die @-Syntax
Statt addiere = logger(addiere) nutzt Python eine kürzere Schreibweise mit dem At-Zeichen:
@logger def addiere(a, b): return a + b print(addiere(2, 3))
@logger direkt vor der Funktion bedeutet exakt: nach der Definition mache addiere = logger(addiere). Mehr Magie steckt nicht dahinter. Die @-Syntax macht den Code lesbar – man sieht direkt am Funktionskopf, dass sie „dekoriert" ist.
4) Decorators mit Argumenten
Manchmal soll ein Decorator selbst Parameter haben – etwa „logge mit Prefix X". Dafür eine weitere Ebene drumherum:
def log_prefix(prefix): def decorator(funktion): def huelle(*args, **kwargs): print(f"[{prefix}] {funktion.__name__}") return funktion(*args, **kwargs) return huelle return decorator @log_prefix("DEBUG") def addiere(a, b): return a + b addiere(2, 3) # [DEBUG] addiere
Drei Ebenen wirken zunächst irritierend. Lies es von außen nach innen: log_prefix("DEBUG") gibt einen Decorator zurück (der nun den Prefix kennt). Dieser Decorator umhüllt dann addiere.
5) functools.wraps – Metadaten erhalten
Ohne weitere Maßnahmen verlieren dekorierte Funktionen ihren Namen und ihre Doku, weil äußerlich nur noch die Hülle sichtbar ist. Das beheben wir mit functools.wraps:
from functools import wraps def logger(funktion): @wraps(funktion) # wichtig! def huelle(*args, **kwargs): print("Aufruf") return funktion(*args, **kwargs) return huelle @logger def addiere(a, b): """Addiert zwei Zahlen.""" return a + b print(addiere.__name__) # addiere – nicht huelle print(addiere.__doc__) # Addiert zwei Zahlen.
Ohne @wraps hieße addiere.__name__ jetzt "huelle". Mit @wraps bleiben Name, Docstring und Signatur erhalten – wichtig für Debugging, Logging und Frameworks, die per Reflection auf Funktionen schauen.
6) Eingebaute Decoratoren
Einige Decoratoren kennst du schon, einige sind nützliche Helfer aus der Standardbibliothek:
cls).self oder cls – gehört nur logisch zur Klasse.dataclasses. Generiert __init__, __repr__, __eq__.@cache, aber mit max. N Einträgen (Least Recently Used wird verdrängt).abc. Markiert Methode als abstrakt (siehe Vererbung und Mehrfachvererbung in Python).7) Typischer Decorator: Timer
Ein praktisches Beispiel – ein Timer-Decorator, der die Ausführungszeit einer Funktion misst:
import time from functools import wraps def timer(funktion): @wraps(funktion) def huelle(*args, **kwargs): start = time.perf_counter() ergebnis = funktion(*args, **kwargs) dauer = time.perf_counter() - start print(f"{funktion.__name__}: {dauer:.4f}s") return ergebnis return huelle @timer def lange_aufgabe(): time.sleep(0.5) return "fertig" lange_aufgabe() # lange_aufgabe: 0.5012s
Solche Decoratoren ergänzen die eigentliche Funktion um Querschnitts-Themen wie Logging, Caching, Timing, Berechtigung – ohne dass der Original-Code geändert werden muss. Web-Frameworks wie Flask und FastAPI nutzen Decoratoren intensiv, um Routes zu registrieren.
8) Was ist ein Generator?
Ein Generator ist eine besondere Funktion, die mehrere Werte nacheinander liefern kann – auf Anfrage, einer nach dem anderen. Statt return verwendet sie yield:
def zaehle_bis(n): i = 1 while i <= n: yield i i += 1 for zahl in zaehle_bis(5): print(zahl) # 1, 2, 3, 4, 5
Was passiert hier? zaehle_bis(5) gibt nicht direkt eine Liste zurück, sondern einen Generator. Erst die for-Schleife fragt einen Wert nach dem anderen ab. Bei jedem yield hält die Funktion an, gibt den Wert zurück und macht beim nächsten Aufruf an dieser Stelle weiter.
9) Lazy: Werte erst auf Anfrage
Der große Unterschied zu einer Liste: ein Generator produziert seine Werte erst, wenn sie gebraucht werden. Das spart Speicher dramatisch:
Alle Werte liegen sofort im Speicher. Bei einer Million Zahlen: viele Megabyte RAM.
Nur der aktuelle Wert wird erzeugt. Bei jedem Schritt: nächste Zahl berechnen. Speicher: konstant winzig.
range in Python 3 ist intern ein Generator-ähnliches Objekt. Auch Builtins wie map, filter und zip liefern Iteratoren – nicht Listen.Das macht Generators perfekt für riesige Datenmengen: streamendes Verarbeiten einer Datei mit Millionen Zeilen, ohne alle gleichzeitig im RAM zu halten:
def zeilen_lesen(pfad): with open(pfad) as f: for zeile in f: yield zeile.strip() for z in zeilen_lesen("riesig.log"): if "FEHLER" in z: print(z) # Datei wird Zeile für Zeile gelesen, nicht komplett geladen
10) yield und Pull-Modell
Genauer betrachtet: ein Generator wird mit next() abgefragt. Bei jedem Aufruf läuft er bis zum nächsten yield, gibt den Wert zurück und pausiert. Ist die Funktion zu Ende, wirft er StopIteration:
for-Schleife macht intern genau das: ruft next() bis StopIteration kommt. Du musst next() in der Praxis selten manuell aufrufen.11) Generator-Expressions
Wie List Comprehensions, aber mit Klammern statt eckigen Klammern. Erzeugt einen Generator statt einer Liste:
# List Comprehension – Liste im Speicher quadrate = [x**2 for x in range(10)] # Generator-Expression – lazy quadrate_gen = (x**2 for x in range(10)) print(sum(x**2 for x in range(1000000))) # Summe – ohne dass die Liste je im Speicher liegt
Praktisch in Verbindung mit sum, any, all, min, max – diese verbrauchen Iteratoren elegant ohne Zwischenspeicher.
12) Iteratoren mit Klassen
Du kannst eigene Iteratoren auch über Dunder-Methoden bauen – mit __iter__ und __next__:
class Zaehler: def __init__(self, n): self.n = n self.i = 0 def __iter__(self): return self def __next__(self): if self.i >= self.n: raise StopIteration self.i += 1 return self.i for x in Zaehler(3): print(x) # 1, 2, 3
In der Praxis nutzt man fast immer Generator-Funktionen mit yield – sie sind kürzer und tun dasselbe. Iterator-Klassen lohnen nur, wenn der Iterator komplexen Zustand verwalten muss.
13) Context Manager – with-Blöcke
Ein Context Manager ist ein Objekt, das Eintritt und Austritt aus einem Block verwaltet – typisch für Ressourcen wie Dateien oder Locks:
with open("daten.txt") as f: inhalt = f.read() # Datei wird automatisch geschlossen, auch bei Exception
Eigene Context Manager kannst du mit einer Klasse (Methoden __enter__ und __exit__) oder eleganter mit dem Decorator @contextmanager bauen:
from contextlib import contextmanager @contextmanager def timer_block(name): import time start = time.perf_counter() yield # Code im with-Block läuft hier dauer = time.perf_counter() - start print(f"{name}: {dauer:.4f}s") with timer_block("Berechnung"): sum(x*x for x in range(1_000_000))
Genau wie Decorator und Generator kombiniert: alles vor yield ist „Setup", alles nach yield ist „Cleanup" – läuft sicher auch bei einer Exception im Block.
14) Häufige Anwendungen
| Konstrukt | Typische Anwendung |
|---|---|
| Decorator | Logging, Timing, Caching, Auth-Checks, Web-Routes, Validierung |
| Generator | Große Dateien zeilenweise, Stream-Verarbeitung, unendliche Sequenzen, Pipelines |
| Context Manager | Dateien, Datenbank-Verbindungen, Locks, Transaktionen, Zeit-Messung |
@cache / @lru_cache | Teure Funktionen, die häufig mit gleichen Argumenten gerufen werden |
15) Häufige Fehler
- Decorator ohne
@wraps: Name, Docstring und Signatur verschwinden – Debugger und Tools werden irre - Generator zweimal durchlaufen: Generatoren sind „verbraucht" nach dem ersten Durchlauf. Eine zweite
for-Schleife ergibt nichts - Generator mit
len(): funktioniert nicht –TypeError: object of type 'generator' has no len(). Vorher zur Liste konvertieren - Funktion mit yield, aber kein
return-Wert genutzt: vergessen, dassyielddie Funktion zum Generator macht – die Funktion gibt nicht den ersten Wert, sondern einen Generator zurück - Decorator-Reihenfolge: bei mehreren Decoratoren wird der unterste zuerst auf die Funktion angewendet – wichtig bei
@property+@classmethod-ähnlichen Kombinationen - Mutable Default in @cache: dict, list als Argument verhindern Caching, da sie nicht hashbar sind.
TypeError: unhashable type - Vergessen, dass Decorator zur Definitionszeit läuft:
@loggerruftlogger(funktion)sofort beim Laden des Moduls, nicht erst beim Aufruf
x = decorator(x) – dann verschwindet die Magie. Generatoren machen Code speicher-effizient bei großen Datenmengen; wenn du eine Liste mit Tausenden Elementen aufbaust nur, um sie einmal zu durchlaufen, sollte das ein Generator sein.
Zusammenfassung
Ein Decorator ist eine Funktion, die eine andere Funktion entgegennimmt, eine Hülle drumherum legt und die Hülle zurückgibt. Die @-Syntax ist nur Kurzschreibweise für funktion = decorator(funktion). functools.wraps erhält Name und Docstring der dekorierten Funktion. Wichtige eingebaute Decoratoren: @property, @classmethod, @staticmethod, @dataclass, @cache, @abstractmethod. Ein Generator ist eine Funktion mit yield, die Werte einer nach dem anderen liefert – lazy und speicher-effizient. Generator-Expressions mit runden Klammern erzeugen Generatoren wie List Comprehensions Listen. Eigene Context Manager baust du mit @contextmanager für sicheres Setup und Cleanup um einen with-Block. Alle drei Konstrukte nutzen das Konzept „Funktionen sind Objekte".
