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östStopIterationaus, 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
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.
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.
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.