Python Generators – speichereffiziente Programmierung

Bei Python Generators handelt es sich um eine spezielle Art von Funktionen in Python. Sie erzeugen Werte Schritt für Schritt und ermöglichen so speichereffizientes Arbeiten.

Was genau sind Python Generators?

Python Generators sind besondere Funktionen, die einen Python Iterator zurückgeben. Die Art, wie Sie Python Generators erstellen, ähnelt einer normalen Funktionsdefinition. Der Unterschied liegt im Detail: Statt einem return-Statement gibt es bei Generators ein sogenanntes yield-Statement. Außerdem implementieren Generatorfunktionen wie Iterarators auch eine next()-Funktion.

Das Schlüsselwort yield

Wenn Sie schon Erfahrung mit anderen Programmiersprachen oder mit Python haben, dann kennen Sie das return-Statement. Dieses wird dazu genutzt, von Funktionen berechnete Werte an die aufrufende Instanz im Programmcode weiterzugeben. Nachdem das return-Statement einer Funktion erreicht wurde, wird die Funktion verlassen und ihre Ausführung ist beendet. Bei Bedarf kann die Funktion einfach wieder aufgerufen werden.

Mit yield verhält es sich anders: Das Schlüsselwort ersetzt das return-Statement in Python Generators. Wenn Sie Ihren Generator nun aufrufen, wird der Wert zurückgegeben, den Sie dem yield-Statement übergeben. Danach wird ein Python Generator allerdings nicht verlassen, sondern lediglich unterbrochen. Der aktuelle Status der Generatorfunktion wird quasi gespeichert. Bei einem erneuten Funktionsaufruf Ihres Generators springen Sie dann an die gespeicherte Stelle.

Einsatzgebiete von Python Generators

Aufgrund der Tatsache, dass Python Generators nach dem Prinzip „Lazy Evaluation“ verfahren und Werte immer erst dann auswerten, wenn sie tatsächlich benötigt werden, eignen sich Generatorfunktionen hervorragend für die Arbeit mit sehr großen Datenmengen.

Eine normale Funktion würde den gesamten Dateiinhalt zunächst in eine Variable und somit in Ihren Speicher laden. Bei großen Datenmengen könnte Ihr lokaler Speicher unter Umständen nicht ausreichen und der Vorgang würde zu einem MemoryError führen. Mit Generatoren lassen sich derartige Probleme leicht umgehen, indem die Datei Zeile für Zeile ausgelesen wird. Das Schlüsselwort yield gibt Ihnen den Wert zurück, den Sie aktuell benötigen, und unterbricht dann die Ausführung der Funktion bis zum nächsten Funktionsaufruf, der eine weitere Zeile der Datei bearbeitet.

Tipp

Viele Webanwendungen erfordern die Verarbeitung großer Datenmengen. Python eignet sich auch für den Einsatz in Webprojekten. Mit Deploy Now können Sie das Erstellen Ihrer Webprojekte beschleunigen, indem Sie von automatischem Deployment und Building via GitHub profitieren.

Doch nicht nur das Handling großer Datenmengen, sondern auch die Arbeit mit Unendlichkeit wird durch Python Generators enorm erleichtert. Da lokaler Speicher endlich ist, sind Generators die einzige Möglichkeit, unendliche Listen oder ähnliches in Python zu erzeugen.

CSV-Dateien mit Python Generators auslesen

Wie erwähnt eignen sich Generators vor allem für die Arbeit mit großen Datenmengen. Das folgende Programm ermöglicht es, eine CSV-Datei speichereffizient Zeile für Zeile auszulesen:

import csv
def csv_lesen(dateiname):
	with open(dateiname, 'r') as datei:
		tmp = csv.reader(datei)
		for zeile in tmp:
			yield zeile

for zeile in csv_lesen('test.csv'):
	print(zeile)

Im Codebeispiel importieren wir zunächst das Modul csv, um Zugriff auf die Python-Funktionen zum Verarbeiten von CSV-Dateien zu haben. Anschließend sehen Sie die Definition eines Python Generators namens „csv_lesen“, die wie Funktionsdefinitionen mit dem Schlüsselwort „def“ beginnt. Nachdem die Datei geöffnet wurde, wird in die Python-for-Loop Zeile für Zeile durch die Datei iteriert. Jede Zeile wird dabei mit dem Schlüsselwort „yield“ zurückgegeben.

Außerhalb der Generatorfunktion werden die Zeilen, die der Python Generator zurückgibt, nacheinander auf der Konsole ausgegeben. Hierfür wird die Python-print-Funktion genutzt.

Unendliche Datenstrukturen mit Python Generators erstellen

Eine unendliche Datenstruktur kann logischerweise nicht lokal auf Ihrem Rechner gespeichert werden. Allerdings sind unendliche Datenstrukturen für einige Anwendungen essenziell. Auch hier helfen Generatorfunktionen weiter, da sie alle Elemente nacheinander bearbeiten und so den Speicher nicht überfluten. Ein Beispiel für eine unendliche Folge natürlicher Zahlen könnte im Python-Code wie folgt aussehen:

def natuerliche_zahlen():
	n = 0
	while True:
		yield n
		n += 1

for zahl in nateurlilche_zahlen():
	print(zahl)

Zunächst wird ein Python Generator namens „natuerliche_zahlen“ definiert, der den Startwert der Variablen „n“ festlegt. Anschließend wird eine Python-while-Schleife gestartet, die endlos läuft. Mit „yield“ wird der aktuelle Wert der Variablen zurückgegeben und die Ausführung der Generatorfunktion unterbrochen. Wenn die Funktion ein weiteres Mal aufgerufen wird, wird die zuvor ausgegebene Zahl um 1 inkrementiert und der Generator wieder so lange durchlaufen, bis der Interpreter auf das Schlüsselwort „yield“ trifft. In der for-Schleife unterhalb der Generatorfunktion werden die vom Generator erzeugten Zahlen ausgegeben. Wird das Programm nicht manuell unterbrochen, läuft es endlos.

Kurzschreibweise für Python Generators

Mit List Comprehensions können Sie Python-Listen in nur einer Zeile Code erstellen. Eine ähnliche Kurzschreibweise gibt es auch für Generatoren. Schauen wir uns einen Generator an, der die Zahlen von 0 bis 9 jeweils um den Wert 1 inkrementiert. Er ähnelt dem Generator, den wir für die Generierung der unendlichen Folge von natürlichen Zahlen genutzt haben.

def natuerliche_zahlen():
	n = 0
	while n <= 9:
		yield n
		n+=1

Wenn Sie diesen Generator in einer Zeile Code schreiben möchten, nutzen Sie ein for-Statement in runden Klammern wie in folgendem Beispiel:

increment_generator = (n + 1 for n in range(10))

Wenn Sie diesen Generator nun ausgeben möchten, erhalten Sie folgende Ausgabe:

<generator object <genexpr> at 0x0000020CC5A2D6C8> 

Ihnen wird also angezeigt, wo in Ihrem Speicher sich das erstellte Generator-Objekt befindet. Um auf die Ausgabe Ihres Generators zuzugreifen, bietet sich die next()-Funktion an:

print(next(increment_generator))
print(next(increment_generator))
print(next(increment_generator))

Dieser Codeabschnitt liefert folgenden Output, bei dem die Zahlen von 0 bis 2 jeweils um 1 inkrementiert wurden:

1
2
3

Generators vs. List Comprehensions

Die Kurzschreibweise von Generatoren erinnert stark an List Comprehensions. Der einzige visuelle Unterschied liegt in der Klammerung: Während Sie bei Comprehensions auf eckige Klammern setzen, nutzen Sie für die Erstellung von Python Generators runde Klammern. Doch intern gibt es einen viel wesentlicheren Unterschied: Der Speicherbedarf von Generators ist viel geringer als der von Listen.

import sys
increment_liste = [n + 1 for n in range(100)]
increment_generator = (n + 1 for n in range(100))
print(sys.getsizeof(increment_liste))
print(sys.getsizeof(increment_generator))

Das obige Programm gibt den Speicherbedarf der Liste und des äquivalenten Generators aus:

912
120

Während die Liste 912 Bytes Speicherplatz benötigt, kommt der Generator mit gerade einmal 120 Bytes aus. Dieser Unterschied wird noch immenser, wenn die zu verarbeitende Datenmenge steigt.