2.31. Iteratoren und Generatoren

Die for-Schleife hat mehr Arbeit geleistet, als es den Anschein hat. Diese Seite behandelt das Iterator-Protokoll, auf dem sie aufbaut, und das Schlüsselwort yield, mit dem Sie eigene Iteratoren erstellen können.

2.31.1. Das Iterator-Protokoll

Jedes Objekt, über das iteriert werden kann, implementiert zwei Methoden:

  • __iter__() – liefert einen Iterator über die Elemente des Objekts zurück.

  • __next__() – liefert auf dem Iterator das nächste Element zurück oder löst StopIteration aus, wenn es keine weiteren gibt.

Das eingebaute iter() ruft __iter__ auf; next() ruft __next__ auf. Eine Liste von Hand durchlaufen:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
Eine for-Schleife ruft __iter__ einmal auf, um einen Iterator zu erhalten, und ruft dann wiederholt __next__ auf, bis StopIteration die Schleife beendet.

for ist syntaktischer Zucker für „rufe einmal __iter__ auf, dann durchlaufe __next__, bis StopIteration“.

Was for x in items: tatsächlich tut:

_it = iter(items)
while True:
    try:
        x = next(_it)
    except StopIteration:
        break
    # ... loop body ...

Jede Liste, jedes Tupel, jeder String, jedes Dict, jedes Set, jedes Dateiobjekt und jeder Generator implementiert bereits __iter__ und __next__ – weshalb sie alle mit for funktionieren.

2.31.2. yield und Generator-Funktionen

Eine Funktion, die eine yield-Anweisung enthält, ist eine Generator-Funktion. Ihr Aufruf führt den Rumpf nicht aus; er liefert ein Generator-Objekt (einen Iterator) zurück, das den Rumpf jeweils bis zum nächsten yield ausführt:

def count_up_to(n):
    i = 0
    while i < n:
        yield i
        i += 1

for value in count_up_to(3):
    print(value)

Ausgabe:

0
1
2

Jeder Aufruf von next() setzt die Funktion bis zum nächsten yield fort, übergibt diesen Wert an den Aufrufer und pausiert dort. Der lokale Zustand (i in diesem Fall) bleibt zwischen den Fortsetzungen erhalten.

Sequenzdiagramm mit zwei Lebenslinien (Aufrufer und Generator-Rumpf). Der Aufrufer ruft count_up_to(3) auf, was einen Generator erzeugt, ohne den Rumpf auszuführen. Jeder folgende next()-Aufruf führt den Rumpf bis zum nächsten yield aus, gibt den gelieferten Wert zurück und pausiert. Der vierte next()-Aufruf läuft über das Ende hinaus und löst StopIteration aus. Die Variable i bleibt über die Pausen hinweg erhalten.

next() führt den Rumpf bis zum nächsten yield aus, gibt den Wert zurück und pausiert. Der lokale Zustand überdauert die Pause.

Generatoren sind der einfachste Weg, eine Sequenz verzögert (lazy) zu erzeugen – es wird keine Liste aufgebaut, Elemente werden nur berechnet, wenn der Verbraucher danach fragt, und die Funktion kann bei Bedarf endlos Elemente liefern.

2.31.3. Verzögerte Pipelines

Generatoren lassen sich gut kombinieren. Die Ausgabe eines Generators kann einen anderen speisen:

def numbers():
    i = 0
    while True:
        yield i
        i += 1

def squares(source):
    for x in source:
        yield x * x

pipeline = squares(numbers())

for v in pipeline:
    if v > 100:
        break
    print(v)

Die Werte fließen einzeln durch die Pipeline – keine Zwischenliste, keine in numbers eingebaute Obergrenze, und der Verbraucher (for v in pipeline) entscheidet, wann gestoppt wird.

Drei Kästen von links nach rechts: numbers(), squares(source) und der for-v-in-pipeline-Verbraucher. Darunter sind drei Zyklen gezeichnet. In jedem Zyklus sendet der Verbraucher eine Anforderung nach links an squares, das eine Anforderung nach links an numbers sendet; numbers liefert einen Wert nach rechts an squares, das seinen quadrierten Wert nach rechts an den Verbraucher liefert.

Jedes next() beim Verbraucher löst eine Anforderung durch die Kette aus; Werte existieren nur, wenn etwas danach fragt.

2.31.3.1. yield from

Eine Schleife, die Elemente aus einem anderen Iterable holt und jedes einzeln liefert, ist häufig genug, dass Python eine Abkürzung bereitstellt. Der Ausdruck yield from iter liefert jeden Wert, den das Iterable erzeugt, der Reihe nach – als hätte der Generator eine for x in iter: yield x-Schleife inline:

def chain(*sources):
    for source in sources:
        yield from source

for v in chain([1, 2, 3], (4, 5), "abc"):
    print(v)

Ausgabe:

1
2
3
4
5
a
b
c

yield from ist exakt äquivalent zur expliziten for-Schleife, nur kürzer, und es propagiert StopIteration aus dem inneren Iterable sauber bis zum äußeren Generator hinauf – nützlich, wenn mehrere Generatoren hintereinander verkettet werden.

2.31.3.2. Wenn yield ausgeht

Über das Ende einer Generator-Funktion hinauszulaufen (oder auf ein explizites return zu treffen) löst automatisch StopIteration aus. Es besteht keine Notwendigkeit, es von Hand auszulösen; die umgebende for-Schleife erkennt es und endet.

Verwenden Sie Generatoren, wenn der erzeugende Code natürlicherweise als Schleife mit einigen yield-Punkten geschrieben wird; verwenden Sie eine einfache List Comprehension, wenn Sie wirklich die gesamte Sequenz im Speicher benötigen.