Kurs
Python-Funktionen haben nicht immer ein return-Statement. Generatorfunktionen sind Funktionen, die statt return das Schlüsselwort yield enthalten.
Diese Funktionen erzeugen Generator-Iteratoren — Objekte, die einen Datenstrom repräsentieren. Die Elemente eines Iterators werden erst dann erzeugt und geliefert, wenn sie gebraucht werden. Diese Art der Auswertung wird häufig als Lazy Evaluation bezeichnet.
Beim Arbeiten mit großen Datensätzen bieten Generatoren eine speicherschonende Alternative zum Ablegen von Daten in Listen, Tupeln und anderen Datenstrukturen, die für jedes einzelne Element Speicher benötigen. Generatorfunktionen können außerdem unendliche Iteratoren erzeugen — etwas, das mit streng ausgewerteten Strukturen wie Listen und Tupeln nicht möglich ist.
Bevor wir loslegen, fassen wir kurz die Unterschiede zwischen Funktionen und Generatoren zusammen:
|
Merkmal |
Funktion |
Generator |
|
Werteerzeugung |
Gibt alle Werte auf einmal zurück |
Liefert Werte nacheinander, bei Bedarf |
|
Ausführung |
Läuft vollständig durch und gibt dann zurück |
Hält nach yield an und macht weiter, wenn der nächste Wert angefordert wird |
|
Schlüsselwort |
return |
yield |
|
Speichereinsatz |
Potentiell hoch, speichert die gesamte Sequenz im Speicher |
Gering, speichert nur aktuellen Wert und Zustand |
|
Iteration |
Mehrfaches Iterieren möglich, erfordert aber das Vorhalten der gesamten Sequenz |
Für Einzelpass-Iteration ausgelegt, effizient bei großen oder unendlichen Sequenzen |
Generatorfunktionen mit Pythons yield erstellen
Der Begriff Generator kann sich in Python auf einen Generator-Iterator oder auf eine Generatorfunktion beziehen. Das sind unterschiedliche, aber verwandte Objekte. In diesem Tutorial verwenden wir der Klarheit halber meist die vollständigen Bezeichnungen.
Schauen wir uns zuerst Generatorfunktionen an. Eine Generatorfunktion sieht einer normalen Funktion ähnlich, enthält aber das Schlüsselwort yield statt return.
Wenn ein Python-Programm eine Generatorfunktion aufruft, entsteht ein Generator-Iterator. Iteratoren liefern auf Anfrage jeweils einen Wert und pausieren ihre Ausführung, bis ein weiterer Wert benötigt wird. Ein Beispiel macht den Unterschied zwischen normalen Funktionen und Generatorfunktionen deutlich:
Eine normale Funktion verwenden
Definieren wir zunächst eine normale Funktion mit einem return-Statement. Sie akzeptiert eine Sequenz von Wörtern und einen Buchstaben und gibt eine Liste mit der Anzahl der Vorkommen des Buchstabens in jedem Wort zurück:
def find_letter_occurrences(words, letter):
output = []
for word in words:
output.append(word.count(letter))
return output
print(
find_letter_occurrences(["apple", "banana", "cherry"], "a")
)
[1, 3, 0]
Die Funktion gibt die Liste 1, 3 und 0 zurück: In apple kommt ein a vor, in banana dreimal und in cherry keines. Dieselbe Funktion lässt sich mit einer List Comprehension kürzer schreiben, statt eine leere Liste zu initialisieren und .append() zu verwenden:
def find_letter_occurrences(words, letter):
return [word.count(letter) for word in words]
Diese normale Funktion gibt bei jedem Aufruf eine Liste mit allen Ergebnissen zurück. Ist die Wortliste jedoch groß, belastet das den Speicher, da das Programm eine neue Liste in derselben Größe wie die Ausgangsliste anlegt und speichert. Wird die Funktion wiederholt mit verschiedenen Eingaben genutzt oder arbeiten ähnliche Funktionen parallel auf denselben Daten, steigt der Speicherdruck schnell an.
Eine Generatorfunktion verwenden
Stattdessen kann eine Generatorfunktion zum Einsatz kommen:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print(output)
<generator object find_letter_occurrences at 0x102935e00>
Die Funktion enthält das yield-Schlüsselwort statt return. Beim Aufruf liefert diese Generatorfunktion ein Generatorobjekt, das wir output zuweisen. Dieses Objekt ist ein Iterator. Es enthält nicht bereits die Ergebnisse, also die Anzahlen pro Wort. Stattdessen erzeugt und liefert der Generator die Werte bei Bedarf. Holen wir den ersten Wert aus diesem Generator-Iterator:
print(next(output))
1
Die eingebaute Funktion next() ist eine Möglichkeit, den nächsten Wert aus einem Iterator zu holen. Weitere Varianten sehen wir später.
Der Code in der Generatorfunktion läuft, bis die Zeile mit yield erreicht ist. In diesem Beispiel startet die for-Schleife ihre erste Iteration und holt das erste Element aus words. Die String-Methode .count() gibt eine Ganzzahl zurück — hier 1, da apple genau ein a enthält. Der Generator liefert diesen Wert, und next(output) gibt ihn zurück.
Der Generator output pausiert nun. Er hat also die erste Iteration der for-Schleife abgeschlossen und die Anzahl der a im ersten Wort ermittelt. Jetzt wartet er, bis er erneut gebraucht wird.
Wird next() erneut mit output aufgerufen, setzt der Generator genau an der Pausenstelle fort:
print(next(output))
3
Der Generator macht an der yield-Zeile der ersten Iteration weiter. Da danach kein weiterer Code in der Schleife steht, springt er zum Schleifenanfang zurück und holt das zweite Element aus words. .count() liefert hier 3, und dieser Wert wird ausgegeben. Anschließend pausiert der Generator wieder.
Der dritte Aufruf von next() setzt die Ausführung erneut fort:
print(next(output))
0
Die zweite Iteration endet, die dritte beginnt. Wieder erreicht der Code die yield-Zeile und liefert diesmal 0, da in cherry kein a vorkommt.
Der Generator pausiert erneut. Erst beim vierten next()-Aufruf entscheidet sich sein weiteres Schicksal:
print(next(output))
Traceback (most recent call last):
...
StopIteration
Die Ausführung setzt nach dem Ende der dritten Iteration fort. Da es in words keine weiteren Elemente gibt, wirft der Generator die Ausnahme StopIteration.
In den meisten Fällen greifst du nicht direkt mit next() auf Generator-Elemente zu, sondern über einen Iterationsprozess. Die Ausnahme StopIteration signalisiert dessen Ende. Mehr dazu im nächsten Abschnitt.
Python bietet noch einen anderen Weg, Generator-Iteratoren zu erzeugen, wenn sich ihre Logik in einem einzelnen Ausdruck darstellen lässt, wie im vorigen Beispiel. Den Generator-Iterator output kannst du auch per Generatorausdruck erzeugen:
words = ["apple", "banana", "cherry"]
letter = "a"
output = (word.count(letter) for word in words)
print(next(output))
print(next(output))
print(next(output))
print(next(output))
1
3
0
Traceback (most recent call last):
...
StopIteration
Der Ausdruck in Klammern, der output zugewiesen wird, ist ein Generatorausdruck und erzeugt einen Generator-Iterator mit demselben Verhalten wie die Generatorfunktion find_letter_occurrences().
Zum Abschluss dieses Abschnitts noch ein weiteres Beispiel, das zeigt, wie die Ausführung bei jedem benötigten Element pausiert und fortgesetzt wird:
def show_status():
print("Start")
yield
print("Middle")
yield
print("End")
yield
status = show_status()
next(status)
Start
Diese Generatorfunktion enthält keine Schleife, sondern drei Zeilen mit yield. Beim Aufruf von show_status() entsteht der Generator-Iterator status. Der erste Aufruf von next(status) startet die Ausführung: Es wird "Start" gedruckt und nach dem ersten yield pausiert. Da hinter yield kein Objekt steht, liefert der Generator None.
Die Ausgabe "Middle" erscheint erst beim zweiten next()-Aufruf:
next(status)
Middle
Nach dem zweiten yield pausiert der Generator erneut. Der dritte Aufruf von next() druckt schließlich "End":
next(status)
End
Der Generator hält am letzten yield an. Beim nächsten Abruf eines Werts von diesem Iterator wird eine StopIteration-Ausnahme ausgelöst:
next(status)
Traceback (most recent call last):
...
StopIteration
Weitere Einsatzmöglichkeiten von Generatoren sehen wir im nächsten Abschnitt.
Mit Generator-Iteratoren arbeiten
Generatorfunktionen erzeugen Generator-Iteratoren, und Iteratoren sind iterierbar. Was heißt das? Jedes Mal, wenn du eine Generatorfunktion aufrufst, entsteht ein Iterator. Da Iteratoren iterierbar sind, kannst du sie in for-Schleifen und anderen Iterationsprozessen verwenden.
next() ist daher nicht die einzige Möglichkeit, auf Elemente eines Iterators zuzugreifen. In diesem Abschnitt lernst du weitere Arbeitsweisen mit Generatoren kennen.
Pythons Iterationsprotokoll mit Generatoren nutzen
Greifen wir eine Generatorfunktion aus dem vorherigen Abschnitt wieder auf:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
for value in output:
print(value)
1
3
0
Statt next() mehrfach aufzurufen, wird der Generator-Iterator output hier in einer for-Schleife verwendet. Iteratoren sind iterierbar, daher funktionieren sie in for-Schleifen. Die Schleife holt Werte, bis keine mehr vorhanden sind.
Anders als Datenstrukturen wie Listen und Tupel lässt sich ein Iterator nur einmal durchlaufen. Führst du dieselbe for-Schleife ein zweites Mal auf demselben Iterator aus, gibt es keine Ausgabe mehr:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print("First attempt:")
for value in output:
print(value)
print("Second attempt:")
for value in output:
print(value)
First attempt:
1
3
0
Second attempt:
Die erste for-Schleife erschöpft den Iterator, danach kann er keine Werte mehr liefern. Wenn du den Generator danach erneut benötigst, musst du einen neuen Generator-Iterator aus der Generatorfunktion erstellen.
Mehrere Generator-Iteratoren können gleichzeitig im Programm existieren:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
first_output = find_letter_occurrences(words, letter)
second_output = find_letter_occurrences(words, letter)
print("First value of first_output:")
print(next(first_output))
print("Values of second_output:")
for value in second_output:
print(value)
print("Remaining values of first_output:")
for value in first_output:
print(value)
First value of first_output:
1
Values of second_output:
1
3
0
Remaining values of first_output:
3
0
Die Generatorfunktion find_letter_occurrences() erzeugt zwei Generator-Iteratoren: first_output und second_output. Beide beziehen sich auf dieselben Ausgangsdaten in words, schreiten aber unabhängig voneinander voran.
Im Beispiel wird zunächst mit next() der erste Wert aus first_output geholt. Der Iterator liefert 1 und pausiert. Anschließend durchläuft eine Schleife second_output. Da dieser Generator noch keinen Wert geliefert hat, iteriert die Schleife über alle seine Werte. Zum Schluss iteriert eine weitere for-Schleife über first_output. Dieser Iterator hat seinen ersten Wert bereits geliefert, daher gibt die Schleife nur die restlichen Werte aus.
Die for-Schleife ist nicht die einzige Möglichkeit, über Generatoren zu iterieren:
print(*find_letter_occurrences(words, letter))
print(sorted(find_letter_occurrences(words, letter)))
1 3 0
[0, 1, 3]
In diesen Beispielen wird die Generatorfunktion direkt im Ausdruck aufgerufen, ohne den Iterator einer Variablen zuzuweisen. Im ersten Fall wird der Iterator per Sternchen-Operator entpackt. Das nutzt dasselbe Iterationsprotokoll wie die for-Schleife.
Im zweiten Fall wird der Generator-Iterator an die eingebaute Funktion sorted() übergeben, die ein iterierbares Argument erwartet. Generatoren sind iterierbar und können überall dort eingesetzt werden, wo Python iteriert.
Unendliche Iteratoren erzeugen
Ein Generator liefert einen Wert und pausiert, bis der nächste benötigt wird. Jedes Mal, wenn ein Wert angefordert wird, läuft der Code in der Generatorfunktion bis zum nächsten yield. In allen bisherigen Beispielen gab es nur endlich viele yield-Stellen. Es ist jedoch möglich, mit einer while-Schleife einen Generator zu erstellen, der unendlich viele Werte liefert. Im folgenden Beispiel gibt der Generator zufällig eine Farbe aus der übergebenen Liste zurück:
import random
def get_color(colors):
while True:
yield random.choice(colors)
output_colors = get_color(["red", "green", "blue"])
print("First two colors:")
print(next(output_colors))
print(next(output_colors))
print("Next 10 colors using a 'for' loop:")
for _ in range(10):
print(next(output_colors))
First two colors:
green
red
Next 10 colors using a 'for' loop:
blue
green
green
green
red
red
red
blue
green
red
Die Generatorfunktion get_color() enthält ein yield innerhalb einer while-Schleife. So trifft der Code bei der Suche nach dem nächsten Wert immer wieder auf ein yield. Der Iterator output_colors liefert unendlich viele Farben, zufällig aus der Eingabeliste gewählt. Dieser Generator wird nie erschöpft.
Unendliche Datenstrukturen wie Listen oder Tupel lassen sich nicht erzeugen. Generatoren ermöglichen hingegen unendliche Iterierbare. Beachte: Verwendest du den Generator direkt in einer for-Schleife, läuft die Schleife ohne Ende.
Fortgeschrittene Generator-Konzepte
Generatoren haben in Python weitere fortgeschrittene Anwendungsfälle. Einige davon sehen wir jetzt.
Objekte in den Generator senden
Generatoren können zusätzliche Daten entgegennehmen, die während der Auswertung genutzt werden. Die Anweisung mit dem Schlüsselwort yield ist ein Ausdruck, der zu einem Wert ausgewertet wird. Dieser Wert kann innerhalb der Generatorfunktion einer Variablen zugewiesen werden. Ein einfaches Beispiel:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(next(output))
print(next(output))
1
The yield expression evaluates to: None
2
The yield expression evaluates to: None
Traceback (most recent call last):
...
StopIteration
Das Python-Schlüsselwort yield bildet einen Ausdruck, der zu einem Wert ausgewertet wird. Dieser Wert innerhalb der Generatorfunktion ist jedoch nicht dasselbe Objekt, das der Generator nach außen liefert. Betrachte den ersten yield-Ausdruck: Der Generator liefert die Ganzzahl 1. Entsprechend zeigt print(next(output)) beim ersten Aufruf 1 an und pausiert die Ausführung des Generators.
Der yield-Ausdruck wird innerhalb des Generators aber ebenfalls zu einem Objekt ausgewertet, das hier der Variablen value zugewiesen wird. In diesem Beispiel wird None zugewiesen. Dasselbe passiert beim zweiten yield. Der dritte next()-Aufruf sorgt dafür, dass die restliche Funktion vollständig ausgeführt wird.
Ersetzen wir den zweiten und dritten next()-Aufruf durch .send(), eine Methode der Generator-Klasse:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(output.send("Here's a value"))
print(output.send("Here's another value"))
1
The yield expression evaluates to: Here's a value
2
The yield expression evaluates to: Here's another value
Traceback (most recent call last):
...
StopIteration
Die Generatorfunktion bleibt unverändert. Der Generator wird mit next() gestartet und liefert zuerst 1. Statt erneut next() aufzurufen, sendet das Programm mit output.send() ein Objekt (hier einen String) in den Generator. Der yield-Ausdruck im Generator wertet diesen String aus und weist ihn value zu. So kann der Generator mit dem gesendeten Wert weiterarbeiten.
Der zweite .send()-Aufruf sendet ein neues Objekt, das erneut value zugewiesen wird. Nach dem letzten print() folgt eine StopIteration, weil keine weiteren yield-Ausdrücke mehr vorhanden sind.
Noch ein Beispiel für .send(): Der folgende Generator zeigt den Kontostand an, der sich jedoch aktualisieren lässt:
def get_balance(start_balance):
balance = start_balance
while True:
amount = yield balance
if amount is not None:
balance += amount
current_balance = get_balance(100)
print(next(current_balance))
print(current_balance.send(10))
print(current_balance.send(-20))
print(next(current_balance))
100
110
90
90
Die Generatorfunktion erwartet beim Aufruf einen Startwert. Der Wert von balance kann sich während der Ausführung ändern. Jedes per .send() gesendete Objekt wird amount zugewiesen. Diese Variable ist entweder None (wenn der Generator ohne gesendeten Wert liefert, z. B. durch next()) oder enthält das per .send() übergebene Objekt.
Der Iterator current_balance startet mit einem Kontostand von 100. Mit next() wird der Generator gestartet und liefert den ersten Wert.
Sobald der Generator läuft, kannst du statt next() auch .send() verwenden. Der Generator addiert den gesendeten Wert zum Kontostand. Wenn kein Wert gesendet wird (z. B. durch erneutes next()), liefert der Generator den unveränderten Kontostand.
Direkt aus einem anderen Iterierbaren liefern
Python-Generatoren können mit der Syntax yield from direkt Werte aus einem anderen Generator oder Iterierbaren liefern. Beispiel für eine Generatorfunktion, die eine geschachtelte Liste abflacht:
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested_list = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(nested_list)))
[1, 2, 3, 4, 5, 6, 7]
Die Generatorfunktion akzeptiert eine Liste, die weitere Listen enthalten kann. Die for-Schleife iteriert über die Elemente. Jedes Element ist entweder ein Wert (hier: eine Ganzzahl) oder eine weitere Liste. Ist es keine Liste, liefert der Generator das Element.
Handelt es sich um eine Liste, ruft der Generator rekursiv flatten() mit der inneren Liste auf. Dadurch entsteht ein weiterer Generator-Iterator, der die innere Liste als Datenquelle nutzt. Würden wir hier yield verwenden, würde der äußere Generator den inneren Generator selbst liefern. Mit yield from liefert der äußere Generator hingegen direkt die Werte des inneren Generators.
Zusammenfassung: yield vs. return
Funktionsdefinitionen mit return und yield wirken ähnlich, verhalten sich aber unterschiedlich. Die wichtigsten Unterschiede im Überblick:
|
Normale Funktion |
Generatorfunktion |
|
|
Schlüsselwort |
return (implizit, wenn nicht explizit genutzt) |
yield |
|
Aufruf |
Führt den Code bis zum return aus und gibt dann den Endwert zurück |
Erzeugt einen Generator-Iterator |
|
Beendigung |
Wird durch das return-Statement beendet |
Wird durch yield pausiert und kann später fortgesetzt werden |
|
Rückgabewert |
Ein einzelnes Objekt (kann eine Datenstruktur sein) |
Generator-Iterator |
|
Yield-Ausdruck |
Nicht anwendbar (ergibt eine Anweisung) |
Ergibt |
|
Einsatzszenarien |
Ideal, um ein finales Ergebnis zurückzugeben |
Ideal, um Datenströme zu erzeugen — besonders für große oder unendliche Sequenzen |
Fazit
Pythons yield-Schlüsselwort wird in Funktionen verwendet, um eine Generatorfunktion zu definieren. Beim Aufruf erzeugen diese Funktionen Generator-Iteratoren. Generatoren sind ein Beispiel für Lazy Evaluation in Python: Ausdrücke werden erst dann ausgewertet, wenn der Wert tatsächlich benötigt wird, nicht schon beim Ausführen des Ausdrucks. Der yield-Ausdruck eignet sich daher ideal, um Datenströme zu erzeugen, deren Werte bei Bedarf generiert werden — ohne sie im Speicher vorhalten zu müssen.
Gerade bei großen Datensätzen mit vielen Operationen zählt Effizienz. Generator-Iteratoren gehören zu den wichtigsten Werkzeugen, um große Datenmengen performant zu verarbeiten.
Wenn du mehr über Python lernen willst, wirf einen Blick auf diesen Python Developer Career Track.
Ich habe Physik und Mathematik auf UG-Ebene an der Universität Malta studiert. Dann zog ich nach London und machte meinen Doktor in Physik am Imperial College. Ich habe an neuartigen optischen Techniken zur Abbildung der menschlichen Netzhaut gearbeitet. Jetzt konzentriere ich mich darauf, über Python zu schreiben, über Python zu kommunizieren und Python zu unterrichten.
