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
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

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.

Sequence diagram with two lifelines (caller and generator body). The caller calls count_up_to(3), which creates a generator without running the body. Each subsequent next() runs the body until the next yield, returns the yielded value, and pauses. The fourth next() falls off the end and raises StopIteration. The variable i is preserved across pauses.

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.

Three boxes left-to-right: numbers(), squares(source), and the for-v-in-pipeline consumer. Three cycles are drawn beneath. In each cycle, the consumer sends a pull request leftward to squares, which sends a pull leftward to numbers; numbers yields a value rightward to squares, which yields its squared value rightward to the consumer.

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.