Generatoren in Python: Speicher sparen mit yield und Lazy Evaluation
Warum du mit yield gigantische Datenmengen verarbeiten kannst, ohne dass dir der Arbeitsspeicher wegläuft – und wie Generatoren wirklich funktionieren.
Stell dir vor, du sollst zehn Millionen Zeilen aus einer Logdatei verarbeiten. Der naive Ansatz lädt alles in eine Liste – und schon quittiert dein Programm den Dienst mit einem MemoryError. Generatoren lösen genau dieses Problem: Sie produzieren Werte erst dann, wenn sie gebraucht werden. Diese Idee nennt man Lazy Evaluation, und sie gehört zu den elegantesten Werkzeugen, die Python zu bieten hat.
Das Schlüsselwort yield
Eine Funktion wird zum Generator, sobald sie irgendwo yield statt return verwendet. Der entscheidende Unterschied: return beendet die Funktion, yield pausiert sie und merkt sich den gesamten Zustand – lokale Variablen, die Position im Code, einfach alles.
def zaehle_bis(n):
i = 0
while i < n:
yield i
i += 1
for zahl in zaehle_bis(5):
print(zahl) # 0, 1, 2, 3, 4
Bei jedem Schleifendurchlauf läuft die Funktion bis zum nächsten yield, gibt den Wert zurück und friert danach ein. Beim nächsten Aufruf macht sie exakt an dieser Stelle weiter. Genau dieses Anhalten und Fortsetzen macht Generatoren so mächtig – und übrigens auch verwandt mit dem Konzept, das englischsprachige Leser im ausführlichen Deep Dive auf pykit.org unter Lazy, Stateful, and Infinite: A Practical Deep Dive into Python Generators and yield nachlesen können. Der Artikel geht besonders schön auf den zustandsbehafteten Charakter von Generatoren ein und ergänzt diese Einführung perfekt.
Warum das Speicher spart
Der Clou: Ein Generator hält immer nur einen einzigen Wert zur Zeit im Speicher. Egal ob du über hundert oder über hundert Millionen Elemente iterierst – der Speicherbedarf bleibt konstant. Vergleiche einmal diese beiden Ansätze:
# Liste: alle Quadratzahlen liegen gleichzeitig im RAM
quadrate_liste = [x * x for x in range(10_000_000)]
# Generator: erzeugt jede Quadratzahl erst bei Bedarf
quadrate_gen = (x * x for x in range(10_000_000))
Der zweite Ausdruck mit den runden Klammern ist ein Generator-Ausdruck. Er sieht aus wie eine List Comprehension, verhält sich aber faul. Die Liste belegt hunderte Megabyte, der Generator nur ein paar Byte.
Unendliche Datenströme
Weil Generatoren nur auf Anfrage produzieren, können sie sogar unendliche Folgen modellieren, ohne jemals abzustürzen:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
gen = fibonacci()
print([next(gen) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Eine solche Endlosschleife wäre mit einer Liste undenkbar – sie würde niemals fertig werden. Mit yield holst du dir einfach so viele Werte, wie du gerade brauchst.
Verkettung und Pipelines
Der wahre Charme von Generatoren zeigt sich, wenn du sie zu Verarbeitungsketten zusammensteckst. Jede Stufe reicht ihre Werte lazy an die nächste weiter, ohne Zwischenlisten anzulegen:
def zeilen(dateiname):
with open(dateiname, encoding="utf-8") as f:
for zeile in f:
yield zeile.rstrip()
def nur_fehler(zeilen):
for z in zeilen:
if "ERROR" in z:
yield z
for fehler in nur_fehler(zeilen("app.log")):
print(fehler)
Diese Pipeline liest die Datei zeilenweise, filtert im Vorbeigehen und hält nie mehr als eine Zeile gleichzeitig im Speicher – selbst bei einer Datei von mehreren Gigabyte.
Fazit
Generatoren sind kein exotisches Nischenfeature, sondern ein Grundpfeiler für sauberen, ressourcenschonenden Python-Code. Wo immer du große oder potenziell unendliche Datenmengen verarbeitest, solltest du an yield denken. Wenn du danach tiefer in die Mechanik einsteigen möchtest, lohnt sich der weiterführende englische Deep Dive auf pykit.org – dort werden auch fortgeschrittene Themen wie send() und Generator-Delegation behandelt.