cat posts/generatoren-und-yield.md
Generatoren und yield wirklich verstehen
Werte auf Abruf statt alle auf einmal: wie yield funktioniert, warum Generatoren Speicher sparen und wozu Generator Expressions, yield from und Pipelines gut sind.
In Teil 11 der Python-Serie sind Generatoren kurz aufgetaucht. Sie verdienen einen eigenen Blick, denn sie gehören zu den elegantesten Werkzeugen in Python.
Generatoren lösen ein sehr alltägliches Problem:
Ich möchte eine Folge von Werten verarbeiten, ohne alle Werte gleichzeitig im Speicher zu halten.
Statt zuerst eine komplette Liste zu bauen, liefern Generatoren Werte auf Abruf. Das nennt man oft lazy evaluation.
Das ist nützlich bei:
- großen Dateien,
- Datenströmen,
- Pipelines,
- unendlichen Folgen,
- schrittweiser Verarbeitung,
- teuren Berechnungen,
- Code, der gut mit
for-Schleifen zusammenarbeiten soll.
Normale Funktionen laufen sofort
Eine normale Funktion läuft beim Aufruf direkt los und gibt mit return einen
Wert zurück:
def liste_bis(bis):
zahlen = []
n = 1
while n <= bis:
zahlen.append(n)
n += 1
return zahlen
Aufruf:
werte = liste_bis(3)
print(werte)
Ausgabe:
[1, 2, 3]
Die Funktion erzeugt zuerst die vollständige Liste. Erst danach bekommt der Aufrufer das Ergebnis.
Bei drei Zahlen ist das egal. Bei zehn Millionen Zahlen kann das viel Speicher kosten.
yield macht aus einer Funktion eine Generatorfunktion
Eine Funktion mit yield ist eine Generatorfunktion:
def zaehle(bis):
n = 1
while n <= bis:
yield n
n += 1
Wichtig: Beim Aufruf läuft der Funktionskörper noch nicht los.
generator = zaehle(3)
print(generator)
Die Ausgabe sieht ungefähr so aus:
<generator object zaehle at 0x...>
zaehle(3) gibt ein Generator-Objekt zurück.
Erst wenn Du den nächsten Wert anforderst, beginnt die Funktion zu laufen:
print(next(generator))
print(next(generator))
print(next(generator))
Ausgabe:
1
2
3
Bei jedem yield passiert Folgendes:
- Die Funktion gibt einen Wert ab.
- Die Funktion pausiert.
- Alle lokalen Variablen bleiben erhalten.
- Beim nächsten
next(...)läuft sie direkt hinter demyieldweiter.
In unserem Beispiel bleibt n also zwischen den Aufrufen erhalten.
Was bei next(...) genau passiert
Schauen wir uns den Ablauf an:
def zaehle_laut(bis):
print("Generator startet")
n = 1
while n <= bis:
print(f"vor yield: {n}")
yield n
print(f"nach yield: {n}")
n += 1
print("Generator ist fertig")
Diese Variante heißt bewusst anders als zaehle von oben – sie ist nur zum
Mitlesen gedacht. Alle späteren Beispiele verwenden wieder das schlichte zaehle.
Verwendung:
generator = zaehle_laut(2)
print("erstellt")
print(next(generator))
print(next(generator))
Ausgabe:
erstellt
Generator startet
vor yield: 1
1
nach yield: 1
vor yield: 2
2
Beim Erzeugen des Generators erscheint noch nicht:
Generator startet
Der Funktionskörper läuft erst beim ersten next(...).
Nach dem ersten yield pausiert der Generator. Beim zweiten next(...) geht es
hinter dem ersten yield weiter:
nach yield: 1
Danach wird n erhöht und der nächste Wert geliefert.
Wenn der Generator fertig ist
Nach dem letzten Wert ist der Generator erschöpft.
generator = zaehle(2)
print(next(generator))
print(next(generator))
print(next(generator))
Der dritte Aufruf führt zu:
Traceback (most recent call last):
...
StopIteration
StopIteration ist das Signal:
Dieser Iterator hat keine weiteren Werte.
Normalerweise musst Du diese Exception nicht selbst behandeln, weil for-
Schleifen das automatisch tun.
Meist nutzt Du for, nicht next
Manuelles next(...) ist gut zum Verstehen. Im Alltag verwendest Du meistens
eine for-Schleife:
for zahl in zaehle(3):
print(zahl)
Ausgabe:
1
2
3
Die for-Schleife ruft intern wiederholt next(...) auf.
Sobald StopIteration ausgelöst wird, beendet die Schleife sich sauber.
Das ist der Grund, warum Du die Exception fast nie siehst, obwohl sie intern zum Iterator-Protokoll gehört.
Generatoren sind Iteratoren
Ein Generator-Objekt ist ein Iterator.
Das bedeutet:
generator = zaehle(3)
print(iter(generator) is generator)
Ausgabe:
True
Der Generator ist also sein eigener Iterator.
Das unterscheidet ihn von vielen Container-Objekten.
Eine Liste ist ein Iterable, aber nicht selbst ihr Iterator:
werte = [1, 2, 3]
iterator = iter(werte)
print(iterator is werte)
Ausgabe:
False
Eine Liste kann beliebig oft neue Iteratoren erzeugen.
Ein Generator hat dagegen einen laufenden Zustand und wird beim Iterieren verbraucht.
Generatoren sind einmalig
Ein Generator kann nur einmal vollständig durchlaufen werden.
generator = zaehle(3)
print(list(generator))
print(list(generator))
Ausgabe:
[1, 2, 3]
[]
Beim ersten list(...) wird der Generator vollständig konsumiert.
Beim zweiten Mal ist er leer.
Wenn Du erneut iterieren möchtest, musst Du einen neuen Generator erzeugen:
print(list(zaehle(3)))
print(list(zaehle(3)))
Ausgabe:
[1, 2, 3]
[1, 2, 3]
Das ist eine der wichtigsten Stolperfallen bei Generatoren.
Lazy Evaluation
Generatoren berechnen Werte erst, wenn sie gebraucht werden.
Vergleiche:
quadrate_liste = [x * x for x in range(10_000_000)]
Diese List Comprehension erzeugt sofort eine Liste mit zehn Millionen Werten.
Ein Generator-Ausdruck sieht fast genauso aus, verwendet aber runde Klammern:
quadrate_generator = (x * x for x in range(10_000_000))
Hier werden die Quadrate nicht sofort berechnet. Sie entstehen erst beim Iterieren.
Beispiel:
summe = sum(x * x for x in range(10_000_000))
sum(...) fragt einen Wert nach dem anderen ab. Es muss keine Liste mit allen
Quadraten gleichzeitig im Speicher liegen.
Das spart Speicher.
Es bedeutet aber nicht automatisch, dass Generatoren immer schneller sind. Eine Liste kann bei kleinen Datenmengen oder mehrfacher Verwendung praktischer und manchmal schneller sein.
Faustregel:
- Liste: wenn Du alle Werte brauchst, mehrfach iterierst oder Indexzugriff benötigst.
- Generator: wenn Du Werte nur einmal nacheinander verarbeitest.
Generator Expressions
Ein Generator-Ausdruck ist die Generator-Variante einer Comprehension.
Liste:
quadrate = [zahl * zahl for zahl in range(1, 6)]
Generator:
quadrate = (zahl * zahl for zahl in range(1, 6))
Verwendung:
for quadrat in quadrate:
print(quadrat)
Ausgabe:
1
4
9
16
25
Generator Expressions sind besonders praktisch als Argument für Funktionen wie:
sum(...)
any(...)
all(...)
max(...)
min(...)
Beispiele:
sum(zahl * zahl for zahl in range(1, 101))
any(raum.gegenstaende for raum in raeume.values())
all(spieler.hp > 0 for spieler in gruppe)
Wenn ein Generator-Ausdruck das einzige Argument einer Funktion ist, brauchst Du keine doppelte Klammer:
sum(x * x for x in range(10))
Statt:
sum((x * x for x in range(10)))
Beides funktioniert, die erste Form ist üblicher.
Generator Expression oder Generatorfunktion?
Generator Expressions sind gut für kurze, klare Transformationen:
namen = (gegenstand.name for gegenstand in inventar)
Oder:
fehlerzeilen = (
zeile
for zeile in zeilen
if "ERROR" in zeile
)
Sobald die Logik länger wird, ist eine Generatorfunktion lesbarer:
def nur_fehler(zeilen):
for zeile in zeilen:
if "ERROR" in zeile:
yield zeile
Als Faustregel:
- Ein kurzer Ausdruck: Generator Expression.
- Mehrere Schritte, Namen, Kommentare oder Fehlerbehandlung: Generatorfunktion.
Lesbarkeit ist wichtiger als maximale Kürze.
Unendliche Folgen
Weil Generatoren Werte nur auf Abruf liefern, können sie endlos sein.
def zaehler(start=1):
zahl = start
while True:
yield zahl
zahl += 1
Verwendung:
zahlen = zaehler(10)
print(next(zahlen))
print(next(zahlen))
print(next(zahlen))
Ausgabe:
10
11
12
Eine normale Funktion mit while True und ohne Rückgabe würde nie fertig werden.
Ein Generator mit while True ist dagegen eine endlose Quelle, aus der Du so
viele Werte nimmst, wie Du brauchst.
Aber Vorsicht:
list(zaehler())
Dieser Code versucht, eine unendliche Liste zu bauen. Das endet nicht sinnvoll.
Endlose Generatoren begrenzen
Das Modul itertools hilft beim Arbeiten mit Iteratoren und Generatoren.
Mit islice(...) kannst Du aus einer endlosen Folge nur einen Ausschnitt nehmen:
from itertools import islice
erste_fuenf = list(islice(zaehler(), 5))
print(erste_fuenf)
Ausgabe:
[1, 2, 3, 4, 5]
islice(generator, 5) nimmt fünf Werte und hört dann auf.
Das ist ein typisches Muster:
for zahl in islice(zaehler(start=100), 3):
print(zahl)
Ausgabe:
100
101
102
Zufällige Ereignisse als Generator
Für den Dungeon können wir eine endlose Ereignisquelle bauen:
import random
def ereignis_strom(ereignisse):
"""Liefert endlos zufällige Ereignisse."""
while True:
yield random.choice(ereignisse)
Verwendung:
ereignisse = [
"Eine Fledermaus flattert durch den Raum.",
"Irgendwo tropft Wasser.",
"Du hörst ein fernes Knurren.",
]
strom = ereignis_strom(ereignisse)
print(next(strom))
print(next(strom))
print(next(strom))
Dieser Generator merkt sich keinen komplizierten State. Er liefert einfach bei jedem Abruf ein neues zufälliges Ereignis.
Für reproduzierbare Tests könntest Du einen eigenen Zufallsgenerator übergeben:
def ereignis_strom(ereignisse, zufall):
while True:
yield zufall.choice(ereignisse)
Dann lässt sich der Zufall im Test kontrollieren:
zufall = random.Random(42)
strom = ereignis_strom(ereignisse, zufall)
Pipelines: Generatoren verketten
Generatoren werden besonders stark, wenn Du sie kombinierst.
Angenommen, wir haben eine Logdatei logbuch.txt:
INFO Spiel gestartet
ERROR Spielstand fehlt
INFO Neuer Spieler
ERROR Ungültiger Raum
Eine Generatorfunktion liest die Zeilen:
from pathlib import Path
def zeilen(pfad):
with Path(pfad).open(encoding="utf-8") as datei:
for zeile in datei:
yield zeile.rstrip("\n")
Eine zweite Funktion filtert Fehler:
def nur_fehler(zeilen):
for zeile in zeilen:
if "ERROR" in zeile:
yield zeile
Verwendung:
for fehler in nur_fehler(zeilen("logbuch.txt")):
print(fehler)
Ausgabe:
ERROR Spielstand fehlt
ERROR Ungültiger Raum
Dabei wird immer nur eine Zeile gleichzeitig verarbeitet.
Es entsteht keine Zwischenliste mit allen Zeilen und auch keine zweite Liste mit allen Fehlern.
Der Datenfluss sieht so aus:
Datei -> zeilen(...) -> nur_fehler(...) -> for-Schleife
Das ist eine Generator-Pipeline.
Pipelines erweitern
Eine weitere Stufe könnte die Meldung bereinigen:
def entferne_level(zeilen):
for zeile in zeilen:
_, _, text = zeile.partition(" ")
yield text
Dann:
pipeline = entferne_level(
nur_fehler(
zeilen("logbuch.txt")
)
)
for meldung in pipeline:
print(meldung)
Ausgabe:
Spielstand fehlt
Ungültiger Raum
Jede Stufe macht genau eine Sache:
- Datei lesen
- Fehler filtern
- Level entfernen
- Ergebnis ausgeben
Das hält den Code testbar und speicherschonend.
Generatoren und Dateien
Der Generator zeilen(...) enthält einen with-Block:
def zeilen(pfad):
with Path(pfad).open(encoding="utf-8") as datei:
for zeile in datei:
yield zeile.rstrip("\n")
Die Datei bleibt geöffnet, solange der Generator aktiv ist.
Wenn die Schleife vollständig durchläuft, wird der with-Block verlassen und die
Datei geschlossen.
Auch wenn der Generator vorzeitig geschlossen oder vom Garbage Collector
aufgeräumt wird, sorgt Python normalerweise dafür, dass ausstehende finally-
Blöcke und damit auch der Cleanup des Context Managers ausgeführt werden.
Trotzdem gilt praktisch:
Generatoren, die Ressourcen offenhalten, solltest Du zeitnah konsumieren und nicht dauerhaft irgendwo liegen lassen.
Wenn Du nur kleine Dateien liest und den Inhalt mehrfach brauchst, ist eine Liste manchmal einfacher.
yield from: Werte weiterreichen
Manchmal soll ein Generator einfach alle Werte aus einem anderen Iterable durchreichen.
Ohne yield from:
def alle_gegenstaende(raeume):
for raum in raeume:
for gegenstand in raum.gegenstaende:
yield gegenstand
Mit yield from:
def alle_gegenstaende(raeume):
for raum in raeume:
yield from raum.gegenstaende
yield from iterable bedeutet:
Liefere alle Werte aus diesem Iterable weiter.
Das ist kürzer und drückt die Absicht klar aus.
Ein einfaches Beispiel:
def erst_a_dann_b():
yield from ["a1", "a2"]
yield from ["b1", "b2"]
print(list(erst_a_dann_b()))
Ausgabe:
['a1', 'a2', 'b1', 'b2']
yield from ist besonders nützlich beim Flattening oder wenn Generatoren aus
kleineren Generatoren zusammengesetzt werden.
Rekursive Generatoren
yield from passt gut zu rekursiven Strukturen.
Beispiel: eine verschachtelte Liste flach machen.
def flach(werte):
for wert in werte:
if isinstance(wert, list):
yield from flach(wert)
else:
yield wert
Verwendung:
werte = [1, [2, 3], [4, [5, 6]]]
print(list(flach(werte)))
Ausgabe:
[1, 2, 3, 4, 5, 6]
Ohne yield from müsstest Du die innere Schleife selbst schreiben:
for innerer_wert in flach(wert):
yield innerer_wert
yield from nimmt Dir dieses Muster ab.
return in Generatoren
In einer normalen Funktion gibt return wert einen Wert an den Aufrufer zurück:
def addiere(a, b):
return a + b
In einem Generator beendet return den Generator.
def generator():
yield 1
return
yield 2
Verwendung:
print(list(generator()))
Ausgabe:
[1]
Das zweite yield wird nie erreicht.
Ein return wert in einem Generator ist spezieller:
def generator():
yield 1
return "fertig"
Der Wert "fertig" wird nicht als normaler nächster Wert in der for-Schleife
ausgegeben.
for wert in generator():
print(wert)
Ausgabe:
1
Intern wird der Rückgabewert als value der StopIteration-Exception
transportiert.
Manuell sichtbar wird das so:
g = generator()
print(next(g))
try:
next(g)
except StopIteration as fehler:
print(fehler.value)
Ausgabe:
1
fertig
Für normalen Anwendungscode ist das selten nötig.
Merke Dir für den Einstieg:
yieldliefert Werte.returnbeendet den Generator.
StopIteration nicht selbst im Generator auslösen
Früher sah man manchmal Code, der in Generatoren selbst StopIteration
ausgelöst hat.
Das solltest Du nicht tun.
Schlecht:
def generator():
yield 1
raise StopIteration
In modernem Python wird ein versehentlich aus dem Generator herauslaufendes
StopIteration in einen RuntimeError umgewandelt.
Richtig ist:
def generator():
yield 1
return
Oder einfach die Funktion natürlich auslaufen lassen:
def generator():
yield 1
Der Iterator-Mechanismus kümmert sich selbst um das Ende.
yield und try/finally
Generatoren können Cleanup-Code enthalten:
def generator():
print("öffne Ressource")
try:
yield 1
yield 2
finally:
print("räume auf")
Wenn der Generator vollständig konsumiert wird:
for wert in generator():
print(wert)
Ausgabe:
öffne Ressource
1
2
räume auf
Auch beim expliziten Schließen wird der finally-Block ausgeführt:
g = generator()
print(next(g))
g.close()
Ausgabe:
öffne Ressource
1
räume auf
Das ist wichtig bei Generatoren, die Dateien, Netzwerkverbindungen oder andere Ressourcen offenhalten.
Für einfache Datenströme musst Du close() selten selbst aufrufen. Es ist aber
gut zu wissen, dass Generatoren aufgeräumt werden können.
Fehler treten erst beim Konsumieren auf
Generatoren sind lazy. Deshalb passieren viele Fehler nicht beim Erzeugen, sondern erst beim Iterieren.
def kaputt():
yield 1
int("viel")
yield 2
Aufruf:
g = kaputt()
print("Generator erstellt")
Ausgabe:
Generator erstellt
Noch kein Fehler.
Beim Konsumieren:
print(next(g))
print(next(g))
Ausgabe:
1
Traceback ...
ValueError: invalid literal for int() with base 10: 'viel'
Das ist beim Debugging wichtig.
Wenn ein Generator scheinbar problemlos erzeugt wird, heißt das noch nicht, dass sein Code fehlerfrei durchläuft. Du musst ihn auch konsumieren.
Generatoren testen
Ein Generator lässt sich gut testen, indem Du seine Werte in eine Liste umwandelst.
def test_zaehle():
assert list(zaehle(3)) == [1, 2, 3]
Für endlose Generatoren begrenzt Du die Anzahl:
from itertools import islice
def test_zaehler():
assert list(islice(zaehler(10), 3)) == [10, 11, 12]
Für Pipelines kannst Du kleine Beispielwerte verwenden:
def test_nur_fehler():
eingabe = [
"INFO Start",
"ERROR Kaputt",
"INFO Ende",
]
assert list(nur_fehler(eingabe)) == ["ERROR Kaputt"]
Generatorfunktionen sind oft gut testbar, weil sie Eingaben als Iterables annehmen und Werte liefern, statt selbst alles zu steuern.
Type Hints für Generatoren
Für einfache Generatoren reicht oft Iterator[T] oder Iterable[T].
from collections.abc import Iterator
def zaehle(bis: int) -> Iterator[int]:
n = 1
while n <= bis:
yield n
n += 1
Das bedeutet:
Diese Funktion liefert einen Iterator über Integer.
Für Funktionen, die nur etwas Iterierbares zurückgeben sollen, ist
Iterable[int] manchmal allgemeiner.
from collections.abc import Iterable
def zahlen() -> Iterable[int]:
return [1, 2, 3]
Wenn Du die speziellen Generatorfähigkeiten wie send(...) oder den
Generator-Rückgabewert typisieren willst, gibt es Generator.
from collections.abc import Generator
def zaehle(bis: int) -> Generator[int, None, None]:
n = 1
while n <= bis:
yield n
n += 1
Die drei Typen bedeuten:
Generator[YieldType, SendType, ReturnType]
Für die meisten einfachen Generatoren ist das zu ausführlich. Iterator[int]
ist meistens völlig ausreichend.
send(...): Werte in den Generator schicken
Generatoren können nicht nur Werte liefern. Man kann ihnen mit send(...) auch
Werte hineinschicken.
Ein kleines Beispiel:
def sammler():
gesamt = 0
while True:
wert = yield gesamt
if wert is not None:
gesamt += wert
Verwendung:
g = sammler()
print(next(g))
print(g.send(5))
print(g.send(10))
Ausgabe:
0
5
15
Das ist ein fortgeschritteneres Feature. Viele Python-Programme verwenden Generatoren nur als einfache Wertelieferanten.
Für den Einstieg musst Du send(...) nicht aktiv nutzen. Wichtig ist nur, dass
yield technisch ein Ausdruck ist: Er kann einen Wert abgeben und beim Fortsetzen
auch wieder einen Wert empfangen.
Generatoren sind keine Magie
Ein Generator ist letztlich ein Objekt, das das Iterator-Protokoll erfüllt.
Diese zwei Dinge sind entscheidend:
iter(generator)
next(generator)
Eine for-Schleife macht im Kern genau das:
iterator = iter(generator)
while True:
try:
wert = next(iterator)
except StopIteration:
break
print(wert)
Du schreibst das normalerweise nicht selbst. Aber dieses Modell erklärt, warum
Generatoren so gut mit for, sum, list, any, all, max, min und vielen
anderen Python-Werkzeugen zusammenspielen.
Wann Generatoren gut passen
Generatoren sind besonders passend, wenn:
- Werte nacheinander verarbeitet werden,
- die Datenmenge groß sein kann,
- nicht alle Werte benötigt werden,
- eine Pipeline aus mehreren Verarbeitungsschritten entsteht,
- eine Folge potenziell unendlich ist,
- die Berechnung eines Werts teuer ist,
- der Code gut mit
for-Schleifen harmonieren soll.
Beispiele:
def lese_zeilen(pfad):
...
def filtere_fehler(zeilen):
...
def erzeuge_zufallsereignisse():
...
def alle_gegenstaende(raeume):
...
Wann eine Liste besser ist
Eine Liste ist oft besser, wenn:
- Du die Werte mehrfach durchlaufen möchtest,
- Du Indexzugriff brauchst,
- Du die Länge oft wissen möchtest,
- Du die Werte sortieren willst,
- Du kleine, überschaubare Datenmengen hast,
- Du das Ergebnis dauerhaft speichern möchtest.
Beispiel:
werte = list(zaehle(10))
Das ist völlig in Ordnung, wenn Du bewusst eine Liste brauchst.
Generatoren sind kein Ersatz für Listen. Sie sind ein anderes Werkzeug.
Stolperfallen
-
Generatoren laufen beim Erzeugen nicht los: Fehler im Generator erscheinen oft erst beim Iterieren.
-
Ein Generator ist nach dem Durchlaufen leer: Für einen zweiten Durchlauf musst Du einen neuen Generator erzeugen.
-
Endlose Generatoren nicht begrenzen:
list(zaehler())versucht eine unendliche Liste zu bauen. -
Generator Expressions mit Listen verwechseln: Runde Klammern erzeugen Werte lazy, eckige Klammern erzeugen sofort eine Liste.
-
Generatoren pauschal für schneller halten: Sie sparen oft Speicher, sind aber nicht automatisch schneller.
-
return wertals normalen Generatorwert erwarten: Der Wert wird nicht in einerfor-Schleife ausgegeben. -
Selbst
StopIterationim Generator auslösen: Verwendereturnoder lass die Funktion auslaufen. -
Ressourcenhaltende Generatoren liegen lassen: Wenn ein Generator eine Datei offenhält, sollte er zeitnah konsumiert oder geschlossen werden.
-
Zu komplexe Generator Expressions schreiben: Wenn der Ausdruck schwer zu lesen wird, ist eine Generatorfunktion besser.
-
Beim Debugging vergessen zu konsumieren: Ein Generator-Objekt zu erzeugen beweist noch nicht, dass sein Inhalt funktioniert.
Übungen
1. Einen einfachen Generator schreiben
Schreibe einen Generator zaehle_bis(bis), der die Zahlen von 1 bis bis
liefert.
Lösung
def zaehle_bis(bis):
zahl = 1
while zahl <= bis:
yield zahl
zahl += 1
print(list(zaehle_bis(5)))
[1, 2, 3, 4, 5]
2. Einen endlosen Generator begrenzen
Schreibe einen Generator zaehler(start), der endlos hochzählt. Gib mit
itertools.islice(...) die ersten fünf Werte aus.
Lösung
from itertools import islice
def zaehler(start):
zahl = start
while True:
yield zahl
zahl += 1
print(list(islice(zaehler(10), 5)))
[10, 11, 12, 13, 14]
3. Eine Generator Expression verwenden
Berechne die Summe der Quadrate von 1 bis 100, ohne zuerst eine Liste zu
bauen.
Lösung
summe = sum(zahl * zahl for zahl in range(1, 101))
print(summe)
4. Fehlerzeilen filtern
Schreibe eine Generatorfunktion nur_fehler(zeilen), die nur Zeilen mit
"ERROR" liefert.
Lösung
def nur_fehler(zeilen):
for zeile in zeilen:
if "ERROR" in zeile:
yield zeile
zeilen = [
"INFO Start",
"ERROR Kaputt",
"INFO Ende",
]
print(list(nur_fehler(zeilen)))
['ERROR Kaputt']
5. yield from verwenden
Schreibe eine Funktion alle_werte(gruppen), die Werte aus mehreren Listen
nacheinander liefert.
Lösung
def alle_werte(gruppen):
for gruppe in gruppen:
yield from gruppe
gruppen = [
[1, 2],
[3, 4],
[5],
]
print(list(alle_werte(gruppen)))
[1, 2, 3, 4, 5]
6. Generatorverbrauch beobachten
Was gibt dieser Code aus?
werte = (zahl for zahl in range(3))
print(list(werte))
print(list(werte))
Lösung
[0, 1, 2]
[]
Vertiefung
- Python lernen, Teil 11: Werkzeugkasten und Ausblick
- Python lernen, Teil 6: Pythonic schreiben
- pytest von Null
- Type Hints in der Praxis
- Debuggen mit breakpoint() und pdb
Fazit
Generatoren sind Funktionen, die Werte nach und nach liefern.
Der entscheidende Unterschied zu Listen ist:
[wert for wert in daten]
baut sofort eine Liste.
(wert for wert in daten)
liefert Werte erst auf Abruf.
Mit yield kannst Du eigene Generatoren schreiben, die gut mit Pythons
Iterator-Protokoll zusammenspielen:
for wert in generator():
...
Das macht Generatoren ideal für große Datenmengen, Pipelines, Dateien und endlose Folgen.
Du musst sie nicht überall einsetzen. Aber wenn Du einmal verstanden hast, dass
yield eine Funktion pausiert und später fortsetzt, wird viel Python-Code
plötzlich klarer: Generator Expressions, itertools, Dateiiteration, Pipelines
und viele APIs, die Werte nicht auf einmal, sondern Schritt für Schritt liefern.
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.