Generatoren in Python: Große Datenmengen effizient verarbeiten mit yield

Generatoren erzeugen Werte erst dann, wenn du sie brauchst – und sparen so massiv Speicher. In diesem Beitrag lernst du, wie yield funktioniert und wann sich Generatoren wirklich lohnen.

Teilen

Stell dir vor, du sollst eine Log-Datei mit mehreren Gigabyte verarbeiten. Liest du sie komplett in eine Liste ein, ist dein Arbeitsspeicher schnell am Limit. Genau für solche Fälle gibt es in Python Generatoren: Funktionen, die Werte nicht auf einmal, sondern Stück für Stück erzeugen – immer erst dann, wenn der nächste Wert wirklich gebraucht wird. Dieses Prinzip nennt sich Lazy Evaluation und ist eines der mächtigsten Werkzeuge für effizienten Python-Code.

Was ist ein Generator?

Ein Generator sieht aus wie eine normale Funktion, benutzt aber das Schlüsselwort yield statt return. Der Unterschied ist fundamental: Während return die Funktion beendet, pausiert yield sie nur. Beim nächsten Aufruf läuft die Funktion genau dort weiter, wo sie stehen geblieben ist – inklusive aller lokalen Variablen.

def countdown(start):
    print("Countdown startet!")
    while start > 0:
        yield start
        start -= 1

zaehler = countdown(3)
print(next(zaehler))  # Countdown startet! -> 3
print(next(zaehler))  # 2
print(next(zaehler))  # 1

Beachte: Der Aufruf countdown(3) führt noch keinen Code aus. Erst das erste next() startet die Funktion und läuft bis zum ersten yield. Jedes weitere next() setzt die Ausführung fort. Sind keine Werte mehr da, wirft der Generator eine StopIteration-Exception – in einer for-Schleife passiert das unsichtbar im Hintergrund.

Der Speichervorteil in der Praxis

Der große Gewinn zeigt sich beim Speicherverbrauch. Eine Liste hält alle Elemente gleichzeitig im Speicher, ein Generator immer nur den aktuellen Zustand:

import sys

# Liste: alle Werte sofort im Speicher
quadrate_liste = [x * x for x in range(1_000_000)]
print(sys.getsizeof(quadrate_liste))  # ca. 8448728 Bytes

# Generator: nur ein winziges Objekt
quadrate_gen = (x * x for x in range(1_000_000))
print(sys.getsizeof(quadrate_gen))    # ca. 200 Bytes

Die zweite Variante ist eine Generator Expression – sie sieht aus wie eine List Comprehension, nur mit runden statt eckigen Klammern. Sie liefert dieselben Werte, aber eben erst auf Anfrage. Für Aggregationen kannst du sie direkt an Funktionen wie sum() oder max() übergeben:

gesamt = sum(x * x for x in range(1_000_000))
print(gesamt)  # 333332833333500000

Hier wird zu keinem Zeitpunkt eine Millionen-Elemente-Liste aufgebaut – die Werte fließen einzeln durch die Summierung.

Ein realistisches Beispiel: Log-Dateien filtern

Zurück zum Anfangsproblem. Mit Generatoren kannst du eine riesige Datei zeilenweise verarbeiten, ohne sie komplett zu laden. Dateiobjekte in Python sind selbst schon Iteratoren, das spielt uns in die Karten:

def fehler_zeilen(dateiname):
    with open(dateiname, encoding="utf-8") as f:
        for zeile in f:
            if "ERROR" in zeile:
                yield zeile.strip()

for fehler in fehler_zeilen("server.log"):
    print(fehler)

Egal ob die Datei 10 Kilobyte oder 10 Gigabyte groß ist – der Speicherverbrauch bleibt konstant, weil immer nur eine Zeile gleichzeitig im Speicher liegt. Solche Generator-Funktionen lassen sich außerdem elegant verketten: Ein Generator filtert, der nächste parst, ein dritter formatiert. So entstehen Verarbeitungs-Pipelines, wie du sie vielleicht von Unix-Pipes kennst.

Generatoren verketten mit yield from

Wenn ein Generator die Werte eines anderen Generators (oder einer beliebigen iterierbaren Struktur) weiterreichen soll, gibt es dafür seit Python 3.3 die Kurzform yield from:

def alle_zahlen():
    yield from range(1, 4)      # 1, 2, 3
    yield from [10, 20]         # 10, 20
    yield 99                    # 99

print(list(alle_zahlen()))  # [1, 2, 3, 10, 20, 99]

Das ist deutlich lesbarer als eine verschachtelte for-Schleife mit einzelnem yield und praktisch, wenn du zum Beispiel rekursiv Verzeichnisbäume durchläufst oder mehrere Datenquellen zu einem Strom zusammenfassen willst.

Wann Liste, wann Generator?

Generatoren sind kein Ersatz für Listen, sondern ein Werkzeug für bestimmte Situationen. Als Faustregel:

  • Generator, wenn du Daten nur einmal durchläufst, die Datenmenge groß oder unbekannt ist oder du eine Pipeline aufbaust.
  • Liste, wenn du mehrfach über die Daten iterieren, per Index zugreifen (daten[5]) oder die Länge mit len() bestimmen willst.

Ein wichtiger Stolperstein: Ein Generator ist nach einem Durchlauf erschöpft. Ein zweites Iterieren liefert keine Werte mehr – wenn du das brauchst, erzeuge den Generator neu oder wandle ihn einmalig mit list() um. Außerdem lohnt ein Blick ins Standardmodul itertools, das viele fertige Bausteine wie islice, chain oder takewhile für die Arbeit mit Iteratoren mitbringt.

Fazit

Generatoren gehören zu den Features, die deinen Python-Code spürbar professioneller machen: Mit yield erzeugst du Werte erst bei Bedarf, hältst den Speicherverbrauch konstant und kannst Verarbeitungsschritte zu sauberen Pipelines verketten. Generator Expressions geben dir dieselbe Kraft in einer einzigen Zeile, und yield from hält verschachtelte Generatoren lesbar. Probiere es beim nächsten Skript aus, das Dateien oder API-Daten verarbeitet – oft reicht es schon, eckige Klammern durch runde zu ersetzen, um aus einem Speicherfresser ein effizientes Programm zu machen.