W3docs

Python-Generatoren und yield

Python-Generatoren und das yield-Schlüsselwort mit klaren Beispielen zu Generatorfunktionen, Ausdrücken, send() und praxisnahen Anwendungsfällen.

Ein Generator ist eine besondere Art von Iterator, der Werte einzeln auf Abruf erzeugt, anstatt sie alle im Voraus zu berechnen. Generatoren werden mit der üblichen Funktions-Syntax definiert, wobei yield anstelle von return verwendet wird. Sie sind die idiomatische Python-Lösung für große oder unendliche Sequenzen, bei denen das Erstellen einer vollständigen Liste Speicher oder Zeit verschwenden würde.

Dieses Kapitel behandelt das Schlüsselwort yield, Generatorfunktionen im Vergleich zu Listen, Generatorausdrücke, das Senden von Werten in einen Generator, das Verketten von Generatoren und praxisnahe Muster.

Was ist ein Generator?

Wenn Python eine reguläre Funktion aufruft, führt sie den Rumpf vollständig aus und gibt einen Wert zurück. Wenn Python eine Generatorfunktion aufruft, führt sie den Rumpf überhaupt nicht aus — stattdessen gibt sie ein Generatorobjekt zurück. Jedes Mal, wenn Sie next() auf diesem Objekt aufrufen, wird die Ausführung dort fortgesetzt, wo sie zuletzt unterbrochen wurde (der yield-Ausdruck), läuft bis zum nächsten yield und hält erneut an.

def count_up(start, stop):
    while start <= stop:
        yield start        # pause here, emit the value
        start += 1

gen = count_up(1, 3)
print(next(gen))   # 1
print(next(gen))   # 2
print(next(gen))   # 3
# next(gen) would now raise StopIteration

Wichtige Mechanismen:

  • Der Funktionsrumpf wird erst beim ersten next()-Aufruf ausgeführt.
  • Lokale Variablen und der Anweisungszeiger werden zwischen den Aufrufen gespeichert.
  • Wenn der Funktionsrumpf endet (oder ein leeres return trifft), löst Python automatisch StopIteration aus.
  • Eine for-Schleife ruft next() für Sie auf und hält bei StopIteration sauber an.

Das yield-Schlüsselwort

yield ist die einzige Syntax, die eine Generatorfunktion von einer regulären unterscheidet. Sie können yield überall verwenden, wo auch return stehen könnte, einschließlich in Schleifen, Bedingungen und try/except-Blöcken.

yield vs return

returnyield
FunktionstypRegulärGenerator
Ausführung nach dem AufrufLäuft bis zum EndeHält bei yield an
Zustand zwischen AufrufenVerworfenErhalten
Mehrere WerteEiner (oder ein Tupel)Einer pro yield, nacheinander
Speicher für große DatenHält alle WerteHält jeweils einen Wert

yield unterbricht, beendet nicht

def three_things():
    print("about to yield first")
    yield "first"
    print("about to yield second")
    yield "second"
    print("about to yield third")
    yield "third"
    print("generator exhausted")

for item in three_things():
    print("got:", item)

Ausgabe:

about to yield first
got: first
about to yield second
got: second
about to yield third
got: third
generator exhausted

Beachten Sie die print-Anweisungen zwischen den yields — normaler Code wird zwischen jeder Unterbrechung ausgeführt.

Generatorfunktionen vs Listen

Betrachten Sie die Erzeugung der ersten n Quadratzahlen. Mit einer Liste:

def squares_list(n):
    result = []
    for i in range(1, n + 1):
        result.append(i * i)
    return result

print(squares_list(5))   # [1, 4, 9, 16, 25]

Mit einem Generator:

def squares_gen(n):
    for i in range(1, n + 1):
        yield i * i

gen = squares_gen(5)
print(list(gen))         # [1, 4, 9, 16, 25]

Beide erzeugen dieselben Werte, aber die Generator-Variante:

  • Verwendet O(1)-Speicher unabhängig von n (die Listenvariante verwendet O(n))
  • Beginnt sofort mit der Werteerzeugung, ohne die gesamte Sammlung erst aufzubauen
  • Kann unendliche Sequenzen darstellen (eine Liste kann das nicht)

Wann einen Generator wählen

Verwenden Sie einen Generator, wenn:

  • Sie die Werte nur einmal durchlaufen müssen.
  • Die Sequenz so groß ist, dass es auf den Speicher ankommt.
  • Sie eine Datenpipeline aufbauen (ein Generator speist den nächsten).
  • Die Sequenz potenziell unendlich ist (z. B. Lesen von Protokollzeilen aus einer Live-Datei).

Verwenden Sie eine Liste, wenn:

  • Sie zufälligen Zugriff per Index benötigen.
  • Sie dieselbe Sequenz mehrmals durchlaufen müssen.
  • Sie len(), Slicing oder In-Place-Sortierung benötigen.

Generatorausdrücke

Ein Generatorausdruck verhält sich zu Generatoren wie ein List Comprehension zu Listen. Die Syntax ist identisch, außer dass runde Klammern statt eckiger Klammern verwendet werden:

# List comprehension — builds the full list immediately
squares_list = [x * x for x in range(1, 6)]

# Generator expression — lazy, produces one value at a time
squares_gen = (x * x for x in range(1, 6))

print(type(squares_list))   # <class 'list'>
print(type(squares_gen))    # <class 'generator'>

print(list(squares_gen))    # [1, 4, 9, 16, 25]

Generatorausdrücke sind am nützlichsten, wenn sie direkt an eine Funktion übergeben werden, die ein Iterable konsumiert:

total = sum(x * x for x in range(1, 101))   # sum of squares 1..100
print(total)   # 338350

Es werden keine zusätzlichen Klammern benötigt, wenn der Generatorausdruck das einzige Argument eines Funktionsaufrufs ist.

Filtern mit Generatorausdrücken

evens = (x for x in range(20) if x % 2 == 0)
print(list(evens))   # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Unendliche Generatoren

Da ein Generator Werte lazy erzeugt, kann er eine Sequenz ohne Ende darstellen. Das klassische Beispiel ist ein unendlicher Zähler:

def counter(start=0):
    n = start
    while True:
        yield n
        n += 1

gen = counter(10)
print(next(gen))   # 10
print(next(gen))   # 11
print(next(gen))   # 12

Um nur einen Teil eines unendlichen Generators zu verarbeiten, verwenden Sie itertools.islice oder verlassen Sie eine Schleife mit break:

import itertools

gen = counter(1)
first_five = list(itertools.islice(gen, 5))
print(first_five)   # [1, 2, 3, 4, 5]

Ein praktischer unendlicher Generator — die Fibonacci-Folge:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

yield from — Delegation an einen Teilgenerator

yield from ermöglicht es einem Generator, an ein anderes Iterable zu delegieren und dabei jeden Wert transparent weiterzuleiten:

def first_part():
    yield 1
    yield 2

def second_part():
    yield 3
    yield 4

def combined():
    yield from first_part()
    yield from second_part()

print(list(combined()))   # [1, 2, 3, 4]

yield from funktioniert auch mit beliebigen Iterables, nicht nur mit Generatoren:

def flatten(nested):
    for sublist in nested:
        yield from sublist

data = [[1, 2], [3, 4], [5, 6]]
print(list(flatten(data)))   # [1, 2, 3, 4, 5, 6]

yield from ist sauberer als eine verschachtelte for-Schleife über das Teil-Iterable und leitet send()- und throw()-Aufrufe korrekt an den delegierten Generator weiter (wichtig für Coroutine-Muster).

Werte in einen Generator senden

Generatoren sind bidirektionale Kanäle. Die Methode .send(value) setzt den Generator fort und übergibt dabei einen Wert als Ergebnis des yield-Ausdrucks zurück:

def accumulator():
    total = 0
    while True:
        value = yield total   # yield sends total out; receives value in
        if value is None:
            break
        total += value

gen = accumulator()
next(gen)          # prime the generator (advance to first yield)
print(gen.send(10))   # 10
print(gen.send(20))   # 30
print(gen.send(5))    # 35

Regeln für .send():

  1. Sie müssen einmal next(gen) (oder gen.send(None)) aufrufen, um den Generator bis zum ersten yield vorzurücken, bevor Sie einen Nicht-None-Wert senden können.
  2. send(None) ist äquivalent zu next().
  3. Der gesendete Wert wird zum Ergebnis des yield-Ausdrucks auf der linken Seite.

Generatorzustand und Erschöpfung

Ein Generatorobjekt hat einen Lebenszyklus mit vier Zuständen:

ZustandBeschreibung
ErstelltGeneratorfunktion aufgerufen, Rumpf noch nicht gestartet
LaufendWird gerade ausgeführt (innerhalb eines next()- oder send()-Aufrufs)
AngehaltenAngehalten bei einem yield; wird beim nächsten next() fortgesetzt
GeschlossenRumpf beendet oder .close() aufgerufen; löst StopIteration aus

Einmal erschöpft, liefert ein erneutes Durchlaufen eines Generators nichts mehr:

gen = (x for x in range(3))
print(list(gen))   # [0, 1, 2]
print(list(gen))   # []  — already exhausted

Wenn Sie die Ausgabe eines Generators mehr als einmal durchlaufen müssen, konvertieren Sie ihn entweder zuerst in eine Liste oder erstellen Sie den Generator neu.

return innerhalb eines Generators

Eine return-Anweisung innerhalb eines Generators beendet die Iteration sauber. Der an return übergebene Wert wird zum value-Attribut der StopIteration-Ausnahme (selten direkt verwendet, aber wichtig für die yield from-Delegation):

def limited():
    yield 1
    yield 2
    return "done"    # StopIteration.value = "done"

gen = limited()
print(next(gen))   # 1
print(next(gen))   # 2
try:
    next(gen)
except StopIteration as e:
    print(e.value)  # done

Praxisnahe Muster

Eine große Datei Zeile für Zeile lesen

def read_lines(filepath):
    with open(filepath) as f:
        for line in f:
            yield line.rstrip("\n")

# Memory usage stays constant regardless of file size
for line in read_lines("/etc/hosts"):
    if line.startswith("#"):
        continue
    print(line)

Eine Datenpipeline aufbauen

Generatoren lassen sich natürlich zu Pipelines zusammensetzen, bei denen jede Stufe den Datenstrom transformiert:

def integers(n):
    for i in range(1, n + 1):
        yield i

def only_even(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

def squared(nums):
    for n in nums:
        yield n * n

# Compose: even squares from 1..20
pipeline = squared(only_even(integers(20)))
print(list(pipeline))
# [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]

Jede Stufe ist lazy — Werte fließen einzeln durch die Pipeline, ohne Zwischenlisten aufzubauen.

Ein Iterable in Blöcke aufteilen

def chunks(iterable, size):
    chunk = []
    for item in iterable:
        chunk.append(item)
        if len(chunk) == size:
            yield chunk
            chunk = []
    if chunk:
        yield chunk

data = list(range(10))
for batch in chunks(data, 3):
    print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]

Generatoren vs Iteratoren vs Comprehensions

MerkmalIterator-KlasseGeneratorfunktionGeneratorausdruck
SyntaxKlasse mit __iter__/__next__def + yield(expr for x in ...)
AusführlichkeitHochGeringSehr gering
ZustandsverwaltungManuellAutomatischAutomatisch
Mehrzeilige LogikJaJaNein (einzelner Ausdruck)
Unendliche SequenzenJaJaJa
Lesbarkeit bei komplexer LogikJaJaNein

Für alles, was über eine einfache Transformation oder Filterung hinausgeht, ist eine Generatorfunktion lesbarer als ein Generatorausdruck. Für komplexe zustandsbehaftete Iteration ist eine Generatorfunktion fast immer einer vollständigen Iterator-Klasse vorzuziehen — siehe Python-Iteratoren für den klassenbasierten Ansatz.

Generatorausdrücke passen gut zu List Comprehensions und Dictionary-/Set-Comprehensions. Dekoratoren können Generatorfunktionen auch umbrechen, um Caching- oder Tracing-Verhalten hinzuzufügen.

Übungen

Übung
Which of the following statements about Python generators are correct?
Which of the following statements about Python generators are correct?
Was this page helpful?