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 StopIterationWichtige 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
returntrifft), löst Python automatischStopIterationaus. - Eine
for-Schleife ruftnext()für Sie auf und hält beiStopIterationsauber 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
return | yield | |
|---|---|---|
| Funktionstyp | Regulär | Generator |
| Ausführung nach dem Aufruf | Läuft bis zum Ende | Hält bei yield an |
| Zustand zwischen Aufrufen | Verworfen | Erhalten |
| Mehrere Werte | Einer (oder ein Tupel) | Einer pro yield, nacheinander |
| Speicher für große Daten | Hält alle Werte | Hä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 exhaustedBeachten 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) # 338350Es 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)) # 12Um 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)) # 35Regeln für .send():
- Sie müssen einmal
next(gen)(odergen.send(None)) aufrufen, um den Generator bis zum erstenyieldvorzurücken, bevor Sie einen Nicht-None-Wert senden können. send(None)ist äquivalent zunext().- 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:
| Zustand | Beschreibung |
|---|---|
| Erstellt | Generatorfunktion aufgerufen, Rumpf noch nicht gestartet |
| Laufend | Wird gerade ausgeführt (innerhalb eines next()- oder send()-Aufrufs) |
| Angehalten | Angehalten bei einem yield; wird beim nächsten next() fortgesetzt |
| Geschlossen | Rumpf 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 exhaustedWenn 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) # donePraxisnahe 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
| Merkmal | Iterator-Klasse | Generatorfunktion | Generatorausdruck |
|---|---|---|---|
| Syntax | Klasse mit __iter__/__next__ | def + yield | (expr for x in ...) |
| Ausführlichkeit | Hoch | Gering | Sehr gering |
| Zustandsverwaltung | Manuell | Automatisch | Automatisch |
| Mehrzeilige Logik | Ja | Ja | Nein (einzelner Ausdruck) |
| Unendliche Sequenzen | Ja | Ja | Ja |
| Lesbarkeit bei komplexer Logik | Ja | Ja | Nein |
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.