Zum Inhalt springen

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:

  1. Die Funktion gibt einen Wert ab.
  2. Die Funktion pausiert.
  3. Alle lokalen Variablen bleiben erhalten.
  4. Beim nächsten next(...) läuft sie direkt hinter dem yield weiter.

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:

yield liefert Werte. return beendet 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 wert als normalen Generatorwert erwarten: Der Wert wird nicht in einer for-Schleife ausgegeben.

  • Selbst StopIteration im Generator auslösen: Verwende return oder 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
Verwendung:
print(list(zaehle_bis(5)))
Ausgabe:
[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)))
Ausgabe:
[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
Test:
zeilen = [
    "INFO Start",
    "ERROR Kaputt",
    "INFO Ende",
]

print(list(nur_fehler(zeilen)))
Ausgabe:
['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
Verwendung:
gruppen = [
    [1, 2],
    [3, 4],
    [5],
]

print(list(alle_werte(gruppen)))
Ausgabe:
[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]
[]
Der Generator wurde beim ersten `list(...)` vollständig verbraucht.

Vertiefung

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.

Weiterlesen

0 Kommentare

Noch keine Kommentare. Sei der/die Erste!