# Generatoren in Python: yield, Iteratoren und Comprehensions für Listen und Dictionaries

URL: https://skillaria.top/de/guides/python/generatory-python

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:
        yield start
        start -= 1

gen = countdown(3)
print(gen)          #
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`, zwei `if` und 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 aus `None`. Für Aktionen nehmen Sie eine `for`-Schleife.
- **Erwartung einer Liste von einem Generatorausdruck.** `(x for x in xs)[0]` ergibt einen `TypeError`, denn Generatoren haben keine Indizes.

## Übungsaufgaben zu Generatoren

1. Schreiben Sie einen Generator `fib()`, der unendlich viele Fibonacci-Zahlen liefert, und holen Sie mit `islice` die ersten 10.
2. Erzeugen Sie in einer Zeile ein Dictionary „Wort → Länge“ für alle Wörter mit mehr als drei Buchstaben.
3. Berechnen Sie die Summe der Quadrate aller geraden Zahlen bis eine Million, ohne eine Liste zu erstellen.
4. Schreiben Sie einen Generator `chunks(seq, n)`, der Teile der Liste mit je `n` Elementen 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.

## FAQ

**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.


---
Source: Skillaria.Top (https://skillaria.top)
