Python-Kurs / Iteratoren und Generatoren mit yield schrittweise verwenden
Ein Iterator liefert auf Anforderung jeweils das nächste Element; mit yield lässt sich ein solcher Ablauf als Generator schreiben. Werte können damit nach Bedarf entstehen, aber ein bereits verbrauchter Iterator beginnt bei erneutem Lesen nicht automatisch von vorn.
Iterierbare Sammlung und aktuellen Durchlauf unterscheiden
Eine Liste ist iterierbar: iter(values) erzeugt einen Iterator, der eine aktuelle Position beim Durchlaufen der Liste besitzt. next(iterator) fordert das nächste Element an. Ist keines mehr vorhanden, meldet er StopIteration. Eine for-Schleife behandelt dieses normale Endsignal und beendet ihren Durchlauf.
Eine gewöhnliche Generatorfunktion enthält yield. Ihr Aufruf erzeugt zunächst ein Generatorobjekt; der Funktionskörper läuft noch nicht bis zum ersten Ergebnis. Erst beim Anfordern eines Wertes wird er ausgeführt und an yield angehalten. Lokale Werte und Ausführungsposition bleiben für die Fortsetzung erhalten. Die Einführung zu Generatoren beschreibt dieses Verhalten.
Das Generatorobjekt ist selbst ein Iterator. Zwei Variablen, die dasselbe Generatorobjekt referenzieren, besitzen daher nicht je einen eigenen Durchlauf. Soll ein neuer unabhängiger Ablauf entstehen, rufen Sie die Generatorfunktion erneut mit einer passenden erneut verfügbaren Quelle auf.
Laufende Summen schrittweise konsumieren
Speichern Sie diesen vollständigen Code als generators.py in UTF-8 im Übungsordner. Starten Sie python generators.py mit dem Projektinterpreter; außerhalb einer aktivierten Umgebung kann unter macOS oder Ubuntu python3 passen. Zusatzpakete und externe Dateien sind nicht erforderlich.
def running_totals(amounts):
total = 0
for amount in amounts:
total += amount
yield total
values = running_totals([3500, 8000, 2500])
print(next(values))
print(list(values))
print(list(values))
print(list(running_totals([3500, 8000, 2500])))
Erwartete Ausgabe:
3500
[11500, 14000]
[]
[3500, 11500, 14000]
Der erste next-Aufruf liefert 3500. list(values) sammelt danach nur die noch verbleibenden Ergebnisse 11500 und 14000. Ein weiteres Sammeln desselben bereits erschöpften Generators ergibt eine leere Liste. Die letzte Ausgabe ruft die Funktion neu mit einer neuen Literalliste auf und erhält deshalb wieder alle drei laufenden Summen.
Die lokale Variable total wird zwischen den yield-Stellen nicht auf null zurückgesetzt. Die Funktion setzt genau dort fort, wo sie nach dem vorherigen Wert angehalten hatte. Dieser gespeicherte Zustand macht die laufende Summierung möglich, ohne vorher eine Liste aller Ausgabewerte zu erzeugen.
Listeniterator und Generatorausdruck vergleichen
Speichern Sie den folgenden zweiten vollständigen Vergleich als iterator.py und führen Sie python iterator.py im selben Ordner aus:
values = [1, 2, 3]
iterator = iter(values)
print(next(iterator))
print(list(iterator))
print(list(iterator))
print(list(values))
expression = (n * 2 for n in values)
print(list(expression))
Erwartete Ausgabe:
1
[2, 3]
[]
[1, 2, 3]
[2, 4, 6]
Der erste Iterator ist nach dem zweiten Sammeln erschöpft, die ursprüngliche Liste enthält ihre Werte aber weiterhin. Ein neuer iter(values)-Aufruf könnte sie erneut durchlaufen. Der Ausdruck (n * 2 for n in values) ist ebenfalls ein Generator und wird beim Sammeln verbraucht. Eine zweite Sammlung dieses selben Ausdrucksobjekts wäre leer.
Die Schreibweise spart das vorzeitige Speichern aller Ergebnisse, nicht automatisch jede Art von Speicher. Eine bereits vorhandene große Eingabeliste benötigt weiterhin ihren Platz. list(generator) speichert zudem sämtliche Ausgabewerte auf einmal. Für einen kleinen mehrfach benötigten Ergebnisbestand kann eine Liste deshalb die einfachere Wahl sein. Aus verzögerter Berechnung folgt keine allgemeine Laufzeitbeschleunigung.
Auch eine neue Generatorhülle setzt eine verbrauchte Eingabequelle nicht zurück. Wenn Sie einen bereits erschöpften Iterator als Eingabe erneut an running_totals übergeben, kann das neue Generatorobjekt daraus keine verlorenen Werte zurückholen. Bewahren Sie entweder die benötigten Ergebnisse auf oder stellen Sie eine tatsächlich erneut lesbare Quelle bereit.
Endsignale, verzögerte Fehler und Ressourcen prüfen
- Ergänzen Sie im ersten Beispiel vor dem ersten
list(values)einen zweitenprint(next(values)). Nun werden 3500 und 11500 einzeln ausgegeben; die restliche Liste enthält nur 14000. Stellen Sie den zusätzlichen Aufruf danach zurück. - Ersetzen Sie die erste Eingabeliste durch eine leere Liste. Die Generatorerzeugung gelingt, der erste
next-Aufruf meldet jedochStopIteration. Wenn ein fehlender Wert erwartbar ist, kannnext(values, None)einen ausdrücklich gewählten Ersatz liefern. Verwenden Sie eine andere eindeutige Markierung, wennNoneselbst ein gültiger Wert wäre; siehe die Referenz zu next. - Ersetzen Sie einen späteren Betrag durch eine nicht addierbare Zeichenkette. Der Generator kann zunächst einen gültigen ersten Wert liefern und erst während der weiteren Sammlung mit
TypeErrorscheitern. Schon verarbeitete Teilergebnisse werden dadurch nicht automatisch zurückgenommen. - Stellen Sie die Eingaben und Aufrufe wieder her und vergleichen Sie beide vollständigen Ausgaben.
Öffnet ein Generator selbst eine Datei und hält innerhalb eines with-Blocks an yield, kann diese Datei zwischen zwei Anforderungen noch offen sein. Ein break in der konsumierenden Schleife schließt ein weiterhin referenziertes Generatorobjekt nicht automatisch zuverlässig in jeder gewünschten Struktur. Legen Sie fest, wer die Lebensdauer beendet, etwa durch einen vom Aufrufer verwalteten Dateiblock oder ausdrücklich kontrolliertes Schließen des Generators. Die Referenz zu generator.close beschreibt das Beenden eines angehaltenen Generators.
Die Übung zum Dateischließen mit with liefert dafür die Ressourcenregel: Ein offenes Objekt soll auch dann passend freigegeben werden, wenn die Verarbeitung früher endet. Testen Sie bei verzögerten Abläufen daher nicht nur das erste Ergebnis, sondern ebenfalls vollständigen Verbrauch, leere Eingabe, einen Fehler während des Verbrauchs und den frühen Abbruch.




