Kodokon kodokon.com

Iteratoren und Generatoren: yield und Pipelines

Meistere das Iterationsprotokoll und Generatoren, um Datenströme lazy und bei konstantem Speicher zu verarbeiten.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Pythons for ist nichts weiter als ein Protokoll: iter(obj) fordert einen Iterator an, dann wird next() aufgerufen, bis zur Ausnahme StopIteration. Unterscheide unbedingt das Iterable (die Liste, die bei jedem Durchgang einen frischen Iterator ausgeben kann) vom Iterator (dem Cursor, der nur einmal durchlaufen werden kann). Dieses Protokoll zu verstehen heißt zu verstehen, warum manche Objekte "aufgebraucht werden".

PYTHON
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
# next(iterator) would raise StopIteration
Was eine for-Schleife tut, von Hand

Eine Funktion, die yield enthält, läuft beim Aufruf nicht: Sie gibt einen Generator zurück, einen Iterator, dessen Rumpf auf Anforderung läuft. Jedes next() rückt bis zum nächsten yield vor, wo die Ausführung samt ihres gesamten lokalen Zustands angehalten wird. Die unmittelbare Folge: Der Speicher bleibt konstant, egal wie groß der Strom ist - du produzierst die Werte einen nach dem anderen, statt eine vollständige Liste zu materialisieren.

PYTHON
def squares(limit):
    n = 0
    while n < limit:
        yield n * n
        n += 1

gen = squares(4)
print(next(gen))  # 0
print(list(gen))  # [1, 4, 9]
yield hält die Funktion zwischen zwei Werten an

Der Hauptnutzen im Produktivbetrieb: Pipelines. Jede Stufe konsumiert den Strom der vorherigen und produziert lazy ihren eigenen; nichts läuft vor der finalen Konsumierung (list(), sum(), ein for). Ein Kompromiss, den du im Kopf behalten solltest: Eine Liste kann mehrfach durchlaufen und geschnitten werden, ein Generator nicht; dafür verarbeitet ein Generator eine 10-GB-Datei, ohne den RAM zu sättigen. Generator-Ausdrücke - (f(x) for x in xs if cond) - decken die einfachen Stufen ab, ohne eine Funktion zu definieren.

PYTHON
lines = ["  12 ", "x", " 7", "", "30"]

def clean(rows):
    for row in rows:
        row = row.strip()
        if row.isdigit():
            yield int(row)

def keep_from(values, floor):
    return (v for v in values if v >= floor)

pipeline = keep_from(clean(lines), 10)
print(list(pipeline))  # [12, 30]
Eine lazy Pipeline: nichts läuft vor list()

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was bekommst du, wenn du eine Funktion aufrufst, deren Rumpf yield enthält?
    • Das Ergebnis des ersten yield
    • Einen Generator, ohne Ausführung des Rumpfes
    • Eine Liste aller erzeugten Werte
  2. Welchen entscheidenden Vorteil hat eine Pipeline aus Generatoren gegenüber Zwischenlisten?
    • Die Ergebnisse werden automatisch sortiert
    • Der Speicher bleibt konstant: Werte werden auf Anforderung erzeugt
    • Die Verarbeitung wird über mehrere Kerne parallelisiert
  3. Was gibt nach einem ersten list(gen) ein zweites list(gen) auf demselben Generator zurück?
    • Dieselbe Liste wie der erste Aufruf
    • Eine leere Liste
    • Eine Ausnahme wird weitergereicht