Generator und yield in Python: Große Datenmengen ohne RAM-Overhead
1. Übersicht
Wenn du mehrere GB große Log-Dateien oder Stream-Daten mit Millionen von Datensätzen aus einer Datenbank verarbeitest, führt die Methode “alles in eine Liste laden und dann verarbeiten” zu enormem RAM-Verbrauch und möglichen Crashes. Dieser Artikel hilft dir zu verstehen, wie generator und yield funktionieren, wie sie sich von normalen Listen unterscheiden, und wie du sie bei praktischen Aufgaben wie dem Lesen großer Dateien oder Data-Processing-Pipelines einsetzt. Nach dem Lesen wirst du Generatoren korrekt schreiben können und wissen, wann du sie nicht verwenden solltest.
2. Das Problem mit dem traditionellen Ansatz
Angenommen, du musst eine 5GB große Log-Datei lesen und die Anzahl der Zeilen zählen, die das Wort “ERROR” enthalten:
def count_errors_bad(filepath: str) -> int:
with open(filepath, "r") as f:
lines = f.readlines() # lädt die GANZE Datei ins RAM
return sum(1 for line in lines if "ERROR" in line)
f.readlines() liest den gesamten Dateiinhalt als Liste von Strings ins Speicher. Bei einer 5GB-Datei reserviert Python ~5GB+ RAM (plus Object-Overhead), bevor es anfängt zu zählen. Wenn der Computer nur 4GB RAM hat, stürzt das Programm mit MemoryError ab oder das System wird durch Swapping eingefroren.
Das Kernproblem: Du brauchst nur eine Zeile nach der anderen zu verarbeiten, aber der Code zwingt den Computer, alle Zeilen gleichzeitig im Speicher zu behalten.
3. Was ist ein Generator?
Ein Generator ist eine spezielle Art von iterable: er erzeugt Werte auf Nachfrage (Lazy Evaluation) statt sie im Voraus zu berechnen und zu speichern. Anstatt eine vollständige Liste zurückzugeben, liefert ein Generator einen Wert nach dem anderen und berechnet den nächsten Wert nur, wenn er abgerufen wird.
def count_errors_good(filepath: str) -> int:
with open(filepath, "r") as f:
return sum(1 for line in f if "ERROR" in line)
Hier ist f (das File-Objekt) bereits ein Iterator — es liest eine Zeile nach der anderen, wenn die Schleife danach fragt, ohne die gesamte Datei ins RAM zu laden. Der Speicherverbrauch ist nur für eine Zeile auf einmal, unabhängig davon, ob die Datei 5GB oder 50GB groß ist.
4. Das Schlüsselwort yield: Wie man eine Generator-Funktion schreibt
Du kannst einen Generator selbst schreiben, indem du yield in einer Funktion verwendest. Wenn eine Funktion yield enthält, konvertiert Python sie automatisch in eine Generator-Funktion — das Aufrufen führt keinen Code aus, sondern gibt ein Generator-Objekt zurück.
def read_large_file(filepath: str):
with open(filepath, "r") as f:
for line in f:
yield line.strip()
yield line.strip()pausiert die Funktion an diesem Punkt und gibtline.strip()nach außen zurück.- Beim nächsten Aufruf von
next()setzt die Funktion ihre Ausführung fort ab dem Pausepunkt, sie startet nicht von vorne. - Der Zustand der Funktion (lokale Variablen, Datei-Cursor-Position) bleibt zwischen den
yield-Aufrufen erhalten.
Probier es aus:
gen = read_large_file("access.log")
print(type(gen))
print(next(gen))
print(next(gen))
<class 'generator'>
127.0.0.1 - GET /index.html 200
127.0.0.1 - GET /style.css 200
Wichtig: Die Zeile gen = read_large_file(...) öffnet die Datei nicht und liest nichts. Der Code darin läuft erst, wenn du zum ersten Mal next(gen) aufrufst.
5. RAM-Vergleich: Generator vs. Liste
Um den Unterschied zu sehen, nutze das sys-Modul zum Messen der Objektgröße:
import sys
def squares_list(n: int) -> list[int]:
return [i * i for i in range(n)]
def squares_gen(n: int):
for i in range(n):
yield i * i
big_list = squares_list(1_000_000)
big_gen = squares_gen(1_000_000)
print(f"List size: {sys.getsizeof(big_list):,} bytes")
print(f"Generator size: {sys.getsizeof(big_gen):,} bytes")
List size: 8,448,728 bytes
Generator size: 208 bytes
Eine Liste mit 1 Million Elementen braucht fast 8,5MB, während der Generator nur ein kleines Objekt von 208 Bytes ist — weil er keine Werte speichert, sondern nur seinen Zustand beibehält, um bei Bedarf den nächsten Wert zu berechnen.
Hinweis: Ein Generator kann nur einmal durchlaufen werden. Nach dem Ende wirft ein erneuter Aufruf von
next()StopIteration. Wenn du mehrfach durchlaufen musst, musst du einen neuen Generator erzeugen, indem du die Funktion erneut aufrufst.
6. Generator Expression: Kompaktere Syntax
Ähnlich wie List Comprehensions, aber mit runden statt eckigen Klammern — du kannst eine Generator Expression schreiben, ohne eine separate Funktion zu definieren:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_squares_gen = (n * n for n in numbers if n % 2 == 0)
print(sum(even_squares_gen))
220
Die runden Klammern () statt [] bewirken, dass Python einen Generator statt einer Liste erzeugt. Das ist praktisch, wenn du den Wert nur einmal brauchst, z.B. direkt für sum(), max() oder any().
7. Datenverarbeitungs-Pipelines mit Generatoren bauen
Die Stärke von Generatoren ist, dass du mehrere Verarbeitungsschritte verketten kannst, ohne zwischenzeitliche Listen zu erzeugen. Angenommen, du musst: CSV-Datei mit Bestellungen einlesen → nur status = "PAID" Bestellungen filtern → amount Summe berechnen.
import csv
def read_orders(filepath: str):
with open(filepath, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
yield row
def filter_paid(orders):
for order in orders:
if order["status"] == "PAID":
yield order
def extract_amount(orders):
for order in orders:
yield float(order["amount"])
def total_paid_amount(filepath: str) -> float:
orders = read_orders(filepath)
paid_orders = filter_paid(orders)
amounts = extract_amount(paid_orders)
return sum(amounts)
Jede Funktion read_orders, filter_paid, extract_amount ist ein Generator — sie verarbeiten nicht alle Daten auf einmal, sondern verbinden sich zu einer Pipeline. Wenn sum(amounts) next() aufruft, wird der Wert durch jeden Schritt gezogen (Pull): eine CSV-Zeile lesen → Status prüfen → zu float konvertieren → aufsummieren. Zu jedem Zeitpunkt existiert nur ein einziger Datensatz im Speicher, unabhängig davon, ob die CSV 10 oder 10 Millionen Zeilen hat.
7.1. Pipeline mit pytest testen
import pytest
from pathlib import Path
@pytest.fixture
def orders_csv(tmp_path: Path) -> str:
content = (
"order_id,amount,status\n"
"1,100.0,PAID\n"
"2,50.0,PENDING\n"
"3,200.0,PAID\n"
)
file_path = tmp_path / "orders.csv"
file_path.write_text(content)
return str(file_path)
def test_total_paid_amount(orders_csv):
result = total_paid_amount(orders_csv)
assert result == pytest.approx(300.0)
tmp_path ist ein eingebautes pytest-Fixture, das für jeden Test ein temporäres Verzeichnis erzeugt. Dieser Test erstellt eine Fake-CSV mit 3 Bestellungen, von denen nur 2 PAID sind, und prüft, dass die Summe 300,0 ist.
8. yield from: Delegation an einen anderen Generator
Wenn ein Generator ein Ergebnis von einem anderen Generator aufrufen und verschachteln muss, hilft yield from dabei, den Code prägnanter zu schreiben, statt eine Schleife manuell zu schreiben.
def read_file_lines(filepath: str):
with open(filepath) as f:
for line in f:
yield line.strip()
def read_multiple_files(filepaths: list[str]):
for filepath in filepaths:
yield from read_file_lines(filepath)
yield from read_file_lines(filepath) ist äquivalent zu for line in read_file_lines(filepath): yield line, aber prägnanter und behandelt auch den Fall korrekt, dass der innere Generator einen Wert mit return zurückgibt. Die Funktion read_multiple_files ermöglicht es dir, mehrere Log-Dateien durchzulaufen, als wären sie ein einziger kontinuierlicher Stream, ohne Listen zusammenzuführen.
9. Werte in einen Generator schicken mit send()
Generatoren geben nicht nur Werte aus — du kannst auch Werte mit send() hineinschicken und ihn dadurch in eine einfache Coroutine umwandeln.
def running_average():
total = 0.0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
avg_gen = running_average()
next(avg_gen) # Generator starten, bis zum ersten yield
print(avg_gen.send(10)) # 10 hineinsenden, average = 10/1
print(avg_gen.send(20)) # 20 hineinsenden, average = 30/2
print(avg_gen.send(30)) # 30 hineinsenden, average = 60/3
10.0
15.0
20.0
Die Zeile value = yield average tut zwei Dinge: average nach außen zurückgeben und gleichzeitig auf einen über send() eingehenden Wert warten, um ihn value zuzuweisen. Du musst next(avg_gen) einmal am Anfang aufrufen, um den Generator zum ersten yield zu bringen — wenn du send() aufrufst, bevor der Generator läuft, wirft Python TypeError.
Hinweis:
send()zu verwenden macht Code schwerer lesbar als ein normaler Generator. In der Praxis haben Frameworks wieasynciodiesen Mechanismus bereits abstrahiert; du brauchst seltensend()zu schreiben, außer wenn du eine eigene Bibliothek entwickelst.
10. Wann solltest du KEINEN Generator verwenden
Generatoren sind nicht immer besser als Listen:
- Du brauchst Zufallszugriff oder mehrfaches Durchlaufen: Generator unterstützt nicht
gen[5]oder erneutes Durchlaufen von vorne. Wenn dulen(), Slicing brauchst oder Daten 2-3 Mal durchlaufen musst, ist eine Liste besser geeignet. - Daten sind klein, RAM ist kein Problem: Bei einigen tausend Elementen ist der Overhead der State-Verwaltung eines Generators manchmal langsamer als ein List Comprehension, weil Listen auf C-Ebene optimiert sind.
- Du brauchst Parallelisierung oder Aufteilung auf mehrere Worker: Listen lassen sich leicht in Chunks aufteilen zum Versenden an Multiprocessing; Generatoren müssen teilweise “materialisiert” werden, bevor du sie aufteilst.
- Debuggen ist schwieriger: Eine Liste ausdrucken zeigt dir alle Werte sofort; ein Generator zeigt nur
<generator object ... at 0x...>, und um Werte zu sehen, musst du ihn durchlaufen (dann ist er “erschöpft” und nicht wiederverwendbar).
Wenn du unsicher bist, hier die einfache Regel: Verwende Liste, wenn Daten ins RAM passen und du sie mehrfach brauchst; verwende Generator, wenn Daten riesig sind, du sie nur sequenziell verarbeiten musst oder eine Streaming-Pipeline baust.
11. Fazit
Generator und yield lösen genau das richtige Problem: Verarbeite große Datenmengen, ohne alles ins RAM zu laden, durch Lazy Evaluation — berechne Werte nur, wenn sie abgerufen werden. Du hast gesehen, wie man Generator-Funktionen schreibt, Generator-Expressions verwendet, mehrere Generatoren mit yield from zu einer Pipeline verbindet, und sogar wie man mit send() Daten rückwärts in einen Generator schickt. Denk daran, dass die Kompromisse real sind: Generatoren sparen Speicher, aber du verlierst die Möglichkeit, nochmal durchzulaufen, wahllos zuzugreifen, und manchmal ist der Code schwerer zu verstehen. Setze Generator nicht überall ein, nur weil sie RAM sparen — wähle das richtige Werkzeug für die richtige Aufgabe.