Iteratoren, Generatoren und Comprehensions
Track Python · PCAP-Fundament · ca. 50 Min.
Worum es geht
Hier liegt viel von dem, was Python-Code kompakt macht. Aus map, filter und function* in JS/TS werden Comprehensions, Iteratoren und Generatoren. Die Quelle zählt das Thema zum Grundwissen. Die Fallen sind fast alle Laufzeitfragen: Ein Generator läuft nur einmal durch, ein Aufruf führt noch nichts aus, und die Schleifenvariable einer Comprehension verschwindet wieder. Nach der Lektion baust du einen eigenen Iterator, sagst das Verhalten von Generatoren voraus und schreibst Comprehensions mit if, else und zwei for-Klauseln.
Ablauf: erst die Übersetzung JS/TS nach Python, dann fünf kleine Schritte mit lauffähigem Code, dann fünf Übungen.
Von JS/TS her gedacht
| Aufgabe | JS/TS | Python |
|---|---|---|
| Jedes Element umwandeln | a.map(x => x * 2) |
[x * 2 for x in a] |
| Filtern | a.filter(x => x > 0) |
[x for x in a if x > 0] |
| Umwandeln und filtern | a.filter(...).map(...) |
[x * 2 for x in a if x > 0] |
| Objekt aus Paaren bauen | Object.fromEntries(...) |
{k: v for k, v in pairs} |
| Einzigartige Werte | new Set(a) |
{x for x in a} |
| Alle Einträge durchlaufen | Object.entries(o) |
d.items() |
| Reduzieren | a.reduce(...) |
functools.reduce(f, a) oder sum(a) |
| Alle oder keiner | a.every(...), a.some(...) |
all(...), any(...) |
| Generator | function* mit yield |
def mit yield |
Der große Unterschied: In JS erzeugt map sofort ein neues Array. In Python gibt es zwei Welten. Die Comprehension in eckigen Klammern baut sofort eine Liste. Die eingebauten Helfer map, filter, zip, enumerate und reversed sowie Generatoren arbeiten lazy (faul): Sie liefern Werte erst, wenn jemand danach fragt.
// TypeScript: Generator mit function*
function* countdown(n: number) {
while (n > 0) {
yield n;
n -= 1;
}
}
const g = countdown(3);
console.log(g.next().value); // 3
console.log([...g]); // [2, 1]
console.log([...g]); // [] (auch in JS erschöpft)Ausgabe: 3, dann [2, 1], dann []. Auch hier ist der Generator nach einem Durchlauf leer, genau wie in JS.
Konzept in kleinen Schritten
Schritt 1: Iterable und Iterator (iterable, iterator)
- Iterable: lässt sich durchlaufen, hat
__iter__. Beispiele: Liste, String, Dict, Set,range, Datei. - Iterator: hat
__next__, liefert Werte einzeln und löst am EndeStopIterationaus. iter(x)macht aus einem Iterable einen Iterator,next(it)holt den nächsten Wert.- Eine
for-Schleife ruft interniter()auf und fängtStopIterationselbst ab.
Die for-Schleife ist also nur diese Schleife in Kurzform:
Eine Liste ist ein Iterable, aber kein Iterator: iter(liste) liefert jedes Mal einen neuen. Ein Iterator ist dagegen sein eigener Iterator:
Ein eigener Iterator ist eine Klasse mit __iter__ (gibt self zurück) und __next__. Das baust du gleich in Übung 1.
Schritt 2: Generatorfunktion (generator function)
Eine Funktion mit yield ist eine Generatorfunktion. Der Aufruf führt noch nichts aus, er liefert ein Generatorobjekt. Jedes next() läuft bis zum nächsten yield und merkt sich den Zustand.
Ausgabe in dieser Reihenfolge: erzeugt, start, 1, ende, []. Der Rumpf lief erst beim ersten next(). Beim zweiten Anlauf (list(h)) lief er bis zum Ende, ohne weiteren Wert.
In JS kennst du das als function* und .next(). Neu ist nur, dass Python for, list(), sum() und andere direkt auf Generatoren arbeiten lässt.
Schritt 3: Generatorausdruck, Erschöpfung, yield from
Ein Generatorausdruck (generator expression) sieht aus wie eine Listen-Comprehension, nur mit runden Klammern. Er spart Speicher, weil die Werte erst bei Bedarf entstehen.
Ein Generator lässt sich nur einmal durchlaufen (Erschöpfung, exhaustion). Und len(g) oder g[0] gehen nicht.
Ausgabe: zwei TypeError-Meldungen, dann 5 0. Das zweite sum(q) findet nichts mehr.
yield from reicht alle Werte eines anderen Iterables durch. Ein return im Generator beendet ihn, der Wert steckt in StopIteration (Attribut value) und ist bei yield from das Ergebnis des Ausdrucks.
Ausgabe: [1, 2, 'fertig'] und 'fertig'.
Schritt 4: Comprehensions im Detail
- Liste:
[ausdruck for x in it if bedingung] - Mit else: Die bedingte Wahl steht vorn:
[x if x > 0 else 0 for x in a]. Ein bloßesifzum Filtern steht hinten. - Verschachtelt:
[x * y for x in a for y in b]entspricht zwei ineinander liegenden Schleifen. Die erstefor-Klausel ist die äußere. - Dict und Set:
{k: v for ...}und{x for ...}. - Eigener Scope (own scope): Die Schleifenvariable ist außerhalb der Comprehension nicht sichtbar (anders als bei der normalen
for-Schleife).
Die Ausgaben: [3, 0, 2, 0], [6, 4], [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')], {'a': 1, 'b': 2} {0, 1, 2}, dann vorher und 2. Die Comprehension hat i nicht überschrieben, die normale Schleife hat j behalten.
Schritt 5: Sortieren und Verknüpfen
sorted(it, key=f, reverse=True)sortiert stabil (stable): Gleiche Werte behalten ihre ursprüngliche Reihenfolge, auch mitreverse=True.keybestimmt den Vergleichswert, oft alslambda(zum Beispielkey=lambda p: p[1]für das zweite Element eines Paares).zip(a, b)bricht beim kürzesten Iterable ab.enumerate(it, start=1)liefert Paare aus Zähler und Wert. Besteht der Wert selbst aus einem Paar, entpackst du verschachtelt:for i, (a, b) in enumerate(paare, start=1).any(it)undall(it)prüfen Wahrheitswerte:all([])istTrue,any([])istFalse.map,zipund Co. sind lazy: erstlist(...)wertet aus, und auch sie sind danach erschöpft.
Ausgabe: ['bb', 'cc', 'a', 'd'] (bei Gleichstand bleibt bb vor cc), [(1, 'a'), (2, 'b')], [(1, 'a'), (2, 'b')], True False, dann ['1', '2'] und [].
Falle: die Prüfungs- und Praxisklassiker
- Zweiter Durchlauf eines Generators ist leer. Das gilt auch für
map,filter,zip,enumerateund jeden Iterator. Eine Liste dagegen lässt sich beliebig oft durchlaufen. - Der Aufruf einer Generatorfunktion führt nichts aus. Fehler und Ausgaben im Rumpf erscheinen erst beim ersten
next(). len(g)undg[0]gehen nicht (TypeError).ifvorn oder hinten? Mitelsesteht die Bedingung vorn, ohneelse(Filtern) hinten. SonstSyntaxError.- Reihenfolge der
for-Klauseln: die erste ist die äußere Schleife. - Comprehension hat eigenen Scope, die normale
for-Schleife nicht. __next__muss am EndeStopIterationauslösen. Wer stattdessenNonezurückgibt, baut eine Endlosschleife.zipschneidet still ab. Bei ungleich langen Eingaben verschwinden Elemente ohne Fehlermeldung.
Übungen
Übung 1: Eigenen Iterator bauen
Schreibe die Klasse Aufsteigend(ende). Ein Objekt liefert nacheinander 1, 2, ..., ende und ist danach erschöpft. Bei ende = 0 liefert es nichts. Die Klasse soll ein echter Iterator sein, also auch iter(objekt) unterstützen und dabei sich selbst zurückgeben.
__iter__ gehört zum Iterable-Protokoll, __next__ zum Iterator-Protokoll. Überlege, was ein Objekt zurückgeben muss, das beides gleichzeitig ist. Und woran erkennt die for-Schleife, dass keine Werte mehr kommen?
class Aufsteigend:
def __init__(self, ende):
self.ende = ende
self.aktuell = 0
def __iter__(self):
return self
def __next__(self):
if self.aktuell >= self.ende:
raise StopIteration
self.aktuell += 1
return self.aktuell
AufsteigendÜbung 2: Generator vorhersagen
Der Code steht fest, du sagst das Ergebnis voraus:
log = []
def zahlen():
log.append("start")
yield 1
log.append("mitte")
yield 2
log.append("ende")
z = zahlen()
l0 = list(log) # Kopie des Logs direkt nach dem Aufruf
next(z)
l1 = list(log)
erg = list(z)
spaeter = list(z)
l2 = list(log)Trage das Tupel (l0, l1, erg, spaeter, l2) ein.
Frage dich bei jeder Zeile: Läuft hier schon Code aus dem Rumpf von zahlen, oder wird nur ein Objekt erzeugt? Und wie weit läuft ein list(z), wenn schon ein Wert entnommen wurde? Was bleibt nach dem Ende übrig?
antwort = ([], ["start"], [2], [], ["start", "mitte", "ende"])
antwortÜbung 3: Comprehension und Scope (Multiple Choice)
i = "vorher"
a = [i for i in range(3)]
for j in range(3):
passWelche Aussage über i und j nach diesem Code stimmt? Gib den Buchstaben als String zurück.
- A:
iist"vorher", aberjlöst einenNameErroraus. - B:
iist2undjist2, beide Schleifen überschreiben. - C:
iist"vorher"undjist2, nur die Comprehension hat Scope. - D:
ilöst einenNameErroraus undjist2, beide sind lokal.
Vergleiche zwei Fragen: Wo lebt die Schleifenvariable bei einer normalen for-Schleife? Und bei einer Comprehension? Beachte auch, dass i vorher schon existiert.
antwort = "C"
antwortÜbung 4: Drei Comprehensions schreiben
Schreibe auswerten(a, b). a und b sind Listen von ganzen Zahlen. Die Funktion liefert ein Tupel aus drei Listen:
verdoppelt: jedes positive Element vona, verdoppelt, in der Reihenfolge vonaohne_minus: alle Elemente vona, wobei negative Zahlen durch0ersetzt werden (die Länge bleibt gleich)paare: alle Paare(x, y)mitxausaundyausb, bei denenx != ygilt. Die Reihenfolge ist die einer verschachtelten Schleife, bei deraaußen undbinnen läuft (also zuerst alle Paare mit dem erstenx).
Nutze Comprehensions.
Bei der ersten Liste fällt etwas weg, bei der zweiten bleibt alles erhalten. Das entscheidet, ob die Bedingung vorn oder hinten steht. Bei der dritten Liste: Welche for-Klausel muss zuerst kommen, damit a die äußere Schleife ist?
def auswerten(a, b):
verdoppelt = [x * 2 for x in a if x > 0]
ohne_minus = [x if x >= 0 else 0 for x in a]
paare = [(x, y) for x in a for y in b if x != y]
return verdoppelt, ohne_minus, paare
auswertenÜbung 5: Rangliste mit sorted, zip und enumerate
Schreibe rangliste(namen, punkte). Beide Parameter sind Listen. Die Funktion liefert eine Liste von Strings der Form "1. Ayse (9)":
- Die Zuordnung ist Position für Position (
namen[0]gehört zupunkte[0]). Ist eine Liste länger, werden die überzähligen Einträge ignoriert. - Sortiert wird nach Punkten, die meisten zuerst. Bei gleichen Punkten bleibt die ursprüngliche Reihenfolge.
- Die Platzierung zählt ab
1. - Die Eingabelisten werden nicht verändert.
Drei eingebaute Helfer passen hier jeweils zu einem Teil der Aufgabe: einer für das Paaren, einer für das Sortieren (schau auf den Parameter für die Richtung), einer für das Zählen mit Startwert. Prüfe an einem Beispiel mit Gleichstand, ob die ursprüngliche Reihenfolge der Gleichen erhalten bleibt, und welche Einstellung von sorted dafür sorgt.
def rangliste(namen, punkte):
paare = zip(namen, punkte)
sortiert = sorted(paare, key=lambda p: p[1], reverse=True)
return [f"{platz}. {name} ({p})" for platz, (name, p) in enumerate(sortiert, start=1)]
ranglisteMerksatz und Prüfstein
Merksatz: Eine Comprehension baut sofort eine Liste, ein Generator liefert Werte erst bei next() und nur ein einziges Mal, und map, zip und enumerate sind genauso faul.
Prüfstein (offene Frage): Du rufst g = zahlen() auf und siehst keine Ausgabe, obwohl im Rumpf ein print steht. Erkläre, warum das so ist und wann die Ausgabe erscheint. Warum liefert sum(g) beim zweiten Mal 0, und wie behebst du das, wenn du die Werte zweimal brauchst?
Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “Iteratoren, Generatoren und Comprehensions” mit “Übersetzungstabelle”, “Begriffe”, “Comprehensions im Detail” und “Sortieren und Verknüpfen”. Hinweis: Die Stabilität von sorted bei reverse=True und das Verhalten von iter(liste) is liste stehen nicht in der Quelle, wurden aber mit Python 3 ausgeführt und geprüft (bitte prüfen, ob sie in der PCAP geprüft werden).