Generatoren in Python: yield, Iteratoren und Comprehensions für Listen und Dictionaries
Ein Generator in Python ist ein Objekt, das Werte einzeln und auf Anfrage liefert und dabei nicht die ganze Folge im Speicher hält. Man erzeugt ihn mit einer Funktion, die yield enthält, oder mit einem Generatorausdruck in runden Klammern. Listen-, Dictionary- und Mengen-Comprehensions sind kurze Schreibweisen zum Aufbau einer fertigen Sammlung in einer Zeile: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.
Ein Generator in Python ist ein Objekt, das Werte einzeln liefert, wenn sie angefordert werden, und dabei nicht die gesamte Folge im Speicher hält. Man erzeugt ihn auf zwei Arten: mit einer Funktion, die das Schlüsselwort yield enthält, oder mit einem Generatorausdruck in runden Klammern. Daneben gibt es Listen-, Dictionary- und Mengen-Comprehensions, eine kurze Schreibweise, um eine fertige Sammlung in einer Zeile aufzubauen. Wir sehen uns an, wie yield funktioniert, worin sich ein Generator von einem Iterator unterscheidet und wie man Comprehensions lesbar hält.
Iteratoren: die Grundlage der for-Schleife
Um Generatoren zu verstehen, muss man zuerst Iteratoren kennen. Ein iterierbares Objekt (iterable) ist alles, was man mit einer Schleife durchlaufen kann: eine Liste, eine Zeichenkette, ein Dictionary, eine Datei. Ein Iterator ist ein Objekt, das seine aktuelle Position merkt und bei jedem Aufruf von next() das nächste Element liefert. Sind alle Elemente abgearbeitet, löst er StopIteration aus.
nums = [10, 20, 30]
it = iter(nums) # Iterator aus der Liste holen
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
next(it) # StopIteration
Genau das tut die Schleife for: Sie ruft iter() auf und danach next(), bis StopIteration kommt. Einen eigenen Iterator können Sie als Klasse mit den Methoden __iter__ und __next__ schreiben:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(3))) # [3, 2, 1]
Das funktioniert, aber es ist viel Gerüstcode. Generatoren erledigen dasselbe in drei Zeilen.
Was ein Generator in Python ist und wie yield funktioniert
Eine Generatorfunktion ist eine gewöhnliche Funktion, in deren Körper ein yield steht. Ihr Aufruf führt den Code nicht aus, sondern liefert ein Generatorobjekt. Jeder Aufruf von next() führt den Körper bis zum nächsten yield aus, gibt den Wert zurück und friert die Funktion zusammen mit ihren lokalen Variablen ein. Der nächste next()-Aufruf setzt genau an dieser Stelle fort.
def countdown(start):
while start > 0:
yield start
start -= 1
gen = countdown(3)
print(gen) # <generator object countdown at 0x...>
print(next(gen)) # 3
print(list(gen)) # [2, 1] — weiter ab der Haltestelle
Erreicht die Funktion ihr Ende (oder ein return), löst der Generator selbst StopIteration aus, und die Schleife for endet sauber. Der Hauptunterschied zwischen yield und return liegt darin: return beendet die Funktion und gibt einen einzigen Wert zurück, yield unterbricht sie nur und kann beliebig viele Werte liefern.
Generator und Iterator: die Unterschiede
Jeder Generator ist ein Iterator, er besitzt __iter__ und __next__. Aber nicht jeder Iterator ist ein Generator. Der Unterschied liegt in der Art der Erzeugung und im Funktionsumfang.
| Iterator (Klasse) | Generator | |
|---|---|---|
| Erzeugung | Klasse mit __iter__ und __next__ | Funktion mit yield oder (x for x in ...) |
| Zustand | von Hand in Attributen | wird automatisch in den lokalen Variablen gespeichert |
| StopIteration | lösen Sie selbst aus | entsteht automatisch beim Verlassen der Funktion |
| Zusätzlich | was Sie selbst schreiben | send(), throw(), close() |
Beide haben eine wichtige Eigenschaft gemeinsam: Man kann sie nur einmal durchlaufen. Ein erneuter for-Durchlauf über einen erschöpften Generator liefert nichts, Sie müssen einen neuen erzeugen.
Unendlicher Generator und das Lesen großer Dateien
Ein Generator muss nicht enden. Eine unendliche Folge ist unbedenklich, solange Sie nur eine begrenzte Anzahl Elemente abrufen:
from itertools import islice
def ids():
n = 1
while True:
yield f"ID-{n:04d}"
n += 1
print(list(islice(ids(), 3))) # ['ID-0001', 'ID-0002', 'ID-0003']
Der praktischste Anwendungsfall ist die zeilenweise Verarbeitung einer großen Datei. Generatoren lassen sich zu einer Kette verbinden. Dabei liegt zu jedem Zeitpunkt nur eine Zeile im Speicher und nicht die ganze Datei:
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.rstrip("\n")
def only_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
for line in only_errors(read_lines("app.log")):
print(line)
yield from und send
yield from gibt alle Werte eines anderen iterierbaren Objekts weiter und spart die verschachtelte Schleife:
def flatten(groups):
for g in groups:
yield from g
print(list(flatten([[1, 2], [3], [4, 5]]))) # [1, 2, 3, 4, 5]
Die Methode send() schickt einen Wert in den Generator hinein: Er wird zum Ergebnis des Ausdrucks yield. Vor dem ersten send muss der Generator mit next() gestartet werden. So schreibt man zum Beispiel einen laufenden Durchschnitt:
def averager():
total, count = 0, 0
avg = None
while True:
value = yield avg
total += value
count += 1
avg = total / count
a = averager()
next(a) # bis zum ersten yield vorlaufen
print(a.send(10)) # 10.0
print(a.send(20)) # 15.0
Im Anwendungscode kommt send selten vor, aber in Vorstellungsgesprächen wird oft danach gefragt.
range: ein Generator?
Nein. range ist eine lazy Folge: Auch er speichert nicht alle Zahlen, unterstützt aber Indexierung, len(), die Prüfung mit in und beliebig viele Durchläufe. Ein Generator kann all das nicht.
r = range(5)
print(r[2], len(r), 3 in r) # 2 5 True
print(list(r), list(r)) # beide Male [0, 1, 2, 3, 4]
Listen-Comprehension
Eine Listen-Comprehension hat die Form [Ausdruck for x in Sammlung]. Sie erzeugt eine neue Liste und verändert die ursprüngliche nicht. Man liest sie von links nach rechts: zuerst, was hineinkommt, dann, woher die Werte stammen.
amounts = [100, 250, 400]
# gewöhnliche Schleife
with_markup = []
for a in amounts:
with_markup.append(a * 1.2)
# dasselbe als Comprehension
with_markup = [a * 1.2 for a in amounts]
print(with_markup) # [120.0, 300.0, 480.0]
Trotz des Namens „Generator“ im Russischen ist das Ergebnis eine fertige Liste und kein Generator: Alle Elemente werden sofort berechnet.
Listen-Comprehension mit if und if else
Hier liegt die häufigste Verwechslung. Eine Bedingung als Filter steht am Ende und wirft Elemente weg. Eine Bedingung zur Auswahl (bedingter Ausdruck) steht am Anfang und macht aus jedem Element einen von zwei Werten, ohne etwas wegzuwerfen.
amounts = [100, 250, 400]
big = [a for a in amounts if a > 200] # [250, 400]
labels = ["viel" if a > 200 else "wenig" for a in amounts]
# ['wenig', 'viel', 'viel']
# Filter und Auswahl zusammen
odd_marks = ["gerade" if n % 2 == 0 else "ungerade" for n in range(6) if n > 2]
# ['ungerade', 'gerade', 'ungerade']
Die Schreibweise [a for a in amounts if a > 200 else 0] ist ein Syntaxfehler: else gibt es nur bei der Bedingung am Anfang.
Verschachtelte Comprehensions und zweidimensionale Listen
Ein doppeltes for schreibt man in derselben Reihenfolge wie verschachtelte Schleifen, von oben nach unten. So verwechselt man beim Zusammenfügen einer Liste aus Listen die Reihenfolge nicht:
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# entspricht:
# for g in groups:
# for x in g:
# flat.append(x)
print(flat) # [1, 2, 3, 4, 5]
Eine zweidimensionale Liste (Matrix) entsteht mit einer Comprehension innerhalb einer Comprehension. Der klassische Fehler ist die Multiplikation einer Liste: Die Zeilen werden zum selben Objekt.
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad) # [[1, 0, 0], [1, 0, 0]] — beide Zeilen haben sich geändert
grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0]]
# Transponieren
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)]) # [[1, 4], [2, 5], [3, 6]]
Dictionary- und Mengen-Comprehension
Dasselbe Prinzip funktioniert für andere Sammlungen. Geschweifte Klammern mit einem Doppelpunkt erzeugen ein Dictionary (Dictionary-Comprehension), ohne Doppelpunkt eine Menge (Mengen-Comprehension).
spent = [("Essen", 500), ("Taxi", 300), ("Essen", 200)]
categories = {cat for cat, _ in spent}
print(categories) # {'Essen', 'Taxi'} — die Reihenfolge einer Menge ist nicht garantiert
last = {cat: amount for cat, amount in spent}
print(last) # {'Essen': 200, 'Taxi': 300}
squares = {n: n ** 2 for n in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}
Beachten Sie das Beispiel last: Bei einem doppelten Schlüssel gewinnt der letzte Wert, die Summe 500 geht verloren. Wenn Sie summieren wollen, ist eine Comprehension ungeeignet. Schreiben Sie dann eine Schleife oder verwenden Sie collections.Counter bzw. defaultdict.
Und eine Tupel-Comprehension?
Eine eigene „Tupel-Comprehension“ gibt es in Python nicht. Runde Klammern erzeugen einen Generatorausdruck, ein Tupel erhält man, wenn man ihn an tuple() übergibt:
t = tuple(n * 2 for n in range(3)) # (0, 2, 4)
Generatorausdruck: nicht speichern, sondern liefern
Ersetzt man die eckigen Klammern durch runde, entsteht ein Generatorausdruck. Er berechnet nichts im Voraus und liefert die Werte einzeln. Bei einer Million Elementen ist der Unterschied im Speicherbedarf enorm:
import sys
as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list)) # etwa 8 MB
print(sys.getsizeof(as_gen)) # einige hundert Bytes
Generatorausdrücke stehen meist innerhalb von Aggregatfunktionen. Ist der Generator das einzige Argument, kann man die zusätzlichen Klammern weglassen. any() und all() brechen außerdem bei der ersten passenden Antwort ab und durchlaufen den Rest nicht.
spent = [("Essen", 500), ("Taxi", 300), ("Essen", 200)]
total = sum(a for _, a in spent) # 1000
has_big = any(a > 400 for _, a in spent) # True, hinter 500 wird nicht weiter geschaut
biggest = max(spent, key=lambda p: p[1]) # ('Essen', 500)
Die Entscheidung ist einfach: Brauchen Sie das Ergebnis mehrmals, Indexierung oder len(), nehmen Sie eine Liste. Wollen Sie einmal durchlaufen und etwas berechnen, nehmen Sie einen Generator.
Gültigkeitsbereich: die Variable sickert nicht durch
Eine Frage, an der selbst erfahrene Entwickler hängen bleiben:
x = 10
squares = [x for x in range(3)]
print(x) # 10
Die Schleifenvariable einer Comprehension lebt in ihrem eigenen Bereich und überschreibt die äußere Variable nicht (in Python 3 ist das so; in Python 2 war es anders). Bei einer gewöhnlichen for-Schleife hätte x danach den Wert 2. Seit Python 3.12 (PEP 709) werden Comprehensions in die umgebende Funktion eingebettet und laufen schneller, die Sichtbarkeitsregel bleibt aber dieselbe.
Entpacken mit * und ** in Comprehensions: neu in Python 3.15 (PEP 798)
Der Stern zum Entpacken war schon immer beim Funktionsaufruf und in Listenliteralen erlaubt. Innerhalb einer Comprehension führte er dagegen zu einem SyntaxError. In Python 3.15 ist diese Einschränkung aufgehoben: *element bedeutet „füge hier alle Elemente ein“. Das funktioniert in Listen, Mengen und Generatoren, auch in asynchronen.
# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups] # [1, 2, 3, 4, 5]
uniq = {*g for g in groups} # Menge
lazy = (*g for g in groups) # fauler Generator
Die doppelte Sternschreibweise macht dasselbe für Dictionaries. Das ist praktisch für geschichtete Einstellungen „Standard → Datei → Flags“: Jede weitere Schicht überschreibt die Schlüssel der vorherigen.
# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}
Zwei Hinweise aus dem Kurs. Der Stern darf nicht ohne Klammern mit einem bedingten Ausdruck kombiniert werden: [*w if w else 0 for w in weeks] ist ein Syntaxfehler, die Bedingung muss in Klammern stehen. Außerdem läuft dieser Code auf Python 3.14 und älter nicht. Wenn eine Bibliothek ältere Versionen unterstützen soll, bleiben Sie beim doppelten for oder nehmen itertools.chain.from_iterable.
Asynchrone Generatoren
Enthält eine Funktion mit async def ein yield, entsteht ein asynchroner Generator. Über ihn iteriert man mit async for, und innerhalb kann man mit await auf Netzwerk oder Festplatte warten. Auch asynchrone Comprehensions gibt es.
import asyncio
async def ticks(n):
for i in range(n):
await asyncio.sleep(0.1)
yield i
async def main():
result = [i async for i in ticks(3)]
print(result) # [0, 1, 2]
asyncio.run(main())
Typische Fehler
- Comprehension mit drei Ebenen. Zwei
for, zweiifund ein bedingter Ausdruck in einer Zeile liest in einer Woche niemand mehr. Passt es nicht mehr in eine Zeile, schreiben Sie eine normale Schleife: PEP 8 stellt Lesbarkeit über Kürze. - Zweiter Durchlauf über einen Generator. Ein zweites
sum(gen)liefert 0, weil der Generator bereits erschöpft ist. - Comprehension wegen eines Seiteneffekts.
[print(x) for x in xs]baut eine unnötige Liste ausNone. Für Aktionen nehmen Sie einefor-Schleife. - Erwartung einer Liste von einem Generatorausdruck.
(x for x in xs)[0]ergibt einenTypeError, denn Generatoren haben keine Indizes.
Übungsaufgaben zu Generatoren
- Schreiben Sie einen Generator
fib(), der unendlich viele Fibonacci-Zahlen liefert, und holen Sie mitislicedie ersten 10. - Erzeugen Sie in einer Zeile ein Dictionary „Wort → Länge“ für alle Wörter mit mehr als drei Buchstaben.
- Berechnen Sie die Summe der Quadrate aller geraden Zahlen bis eine Million, ohne eine Liste zu erstellen.
- Schreiben Sie einen Generator
chunks(seq, n), der Teile der Liste mit jenElementen liefert.
Fazit
Ein Iterator liefert Elemente einzeln über next(). Ein Generator ist der einfachste Weg, einen zu schreiben: eine Funktion mit yield oder ein Ausdruck in runden Klammern. Listen-, Dictionary- und Mengen-Comprehensions bauen fertige Sammlungen in einer Zeile. Der Filter if steht am Ende, die Auswahl if else am Anfang. Wenn Sie Speicher sparen oder einen Datenstrom einmal verarbeiten möchten, nehmen Sie einen Generator, und wenn Sie das Ergebnis mehrfach brauchen, eine Liste. Comprehensions, Generatoren und das neue Entpacken aus Python 3.15 werden im Kurs am Beispiel des Ausgabentrackers praktisch durchgespielt.
Häufige Fragen
Kann ich in einem Generator return verwenden?
Ja. return ohne Wert beendet den Generator einfach, mit einem Wert legt er ihn im Attribut value der Ausnahme StopIteration ab. Diesen Wert erhält ein Ausdruck yield from.
Wie wandle ich einen Generator in eine Liste um?
Übergeben Sie ihn an list(): list(gen). Danach ist der Generator erschöpft, und alle Werte liegen im Speicher.
Ist eine Listen-Comprehension schneller als eine Schleife mit append?
Meist ja, etwas, weil der Aufruf der Methode append bei jedem Durchlauf entfällt. Die Wahl sollte aber nach Lesbarkeit fallen, der Unterschied ist selten entscheidend.
Wie erkenne ich, ob ein Objekt ein Generator ist?
Mit inspect.isgenerator(obj) für ein Objekt oder inspect.isgeneratorfunction(f) für eine Funktion. Ob ein Objekt ein Iterator ist, prüfen Sie mit isinstance(obj, collections.abc.Iterator).
Worin unterscheidet sich ein Generatorausdruck von einer Listen-Comprehension?
Eckige Klammern erzeugen sofort die ganze Liste im Speicher, runde Klammern einen Generator, der die Elemente auf Anfrage berechnet und nur einmal durchlaufen werden kann.
Dieses Thema wird im Kurs „Python 3.15: от новичка до профи“ ausführlich mit Praxis behandelt.
Zum Kurs