Wie was – Iterable und Iterator
Eine Klasse, die gleichzeitig ein Iterable und ein Iteratorist, ist beispielhaft in Abbildung 1 dargestellt. Ein Iterable ist ein Objekt, dass die Methode __iter__()implementiert. Dies sind alle Sequence-Typen (Listen, Strings usw.) und z. B. Dictionaries oder Dateiobjekte. Die Methode wird aufgerufen, wenn ein Iterator-Objekt des Containers benötigt wird. Dies wird von der Methode zurückgegeben. Das Iterator-Objekt wiederum implementiert die Methode __next__(), die jeweils das nächste Element in dem Container zurück gibt bzw. die Exception StopIteration generiert, wenn keine weiteren Elemente im Container vorhanden sind. Damit der Iterator weiß, welches das nächste Element ist, speichert er seinen jeweiligen aktuellen Zustand.
Eine Klasse, die gleichzeitig ein Iterable und ein Iterator ist, kann wie folgt aussehen. Dies entspricht der Implementierung von Generatoren und auch den Objekten, die z. B. map() zurückgibt. Die in dem Beispiel gezeigte Klasse ist ein einfacher Nachbau der Funktion range(). Beim Aufruf von __iter__() gibt sie sich selbst zurück und beim Aufruf von __next__() wird self.start so lange inkrementiert und zurückgegeben, bis self.stop erreicht ist.
Iterables können z. B. in for-Schleifen verwendet werden. Diese ruft implizit die Funktion iter(), die den Iterator zu einem Iterable zurückgibt, mit dem angegebenen IterableObjekt als Argument auf und weist den Iterator einer anonymen Variablen zu. Diese Variable ist temporär und existiert, bis die Schleife beendet ist. In jedem Durchlauf der Schleife wird implizit die Funktion next() mit dem Iteratorals Argument aufgerufen, bis die StopIteration Exception auftritt. Eine for-Schleife lässt sich somit mit einer while-Schleife simulieren (siehe Abbildung 2).
Abb. 1: Iterable- und Iterator-Klasse
Ressourcen? Habe ich doch ausreichend ..
Ein Iterator erzeugt ein Ergebnis nur, wenn es angefordert wird, anstatt alle Ergebnisse sofort bereitzustellen. Dieses „faule" Verhalten kann bei der Verarbeitung großer Datenmengen den benötigten Speicher massiv reduzieren und auch die Performance steigern, wenn komplexe Berechnungen zum Produzieren der Werte notwendig sind. Man kann argumentieren, dass Ressourcen in heutiger Zeit in vielen Fällen kein wesentlicher Faktor sind, aber wenn man ihre Nutzung ohne höheren Aufwand auf ein Minimum reduzieren kann, sollte jeder hellhörig werden. In dem folgenden Beispiel wird im ersten Schritt mittels List-Comprehension eine Liste generiert, die Zweierpotenzen der Zahlen von 0 bis 99999999 enthält, und im zweiten Schritt die Summe dieser Zahlen gebildet:
Für diese Berechnung wird ca. 4 GB Speicher benötigt. Wird anstelle der Liste ein Iterator verwendet, kann der Speicherbedarf, bei vergleichbarer Performance auf ca. 8 MB reduziert werden. Die Implementierung ist im folgenden Beispiel mit einer Generator-Expression gelöst, die in den nächsten Punkten detailliert erläutert wird.
Generatoren
Die Erzeugung eines IteratorObjektes wie im ersten Beispiel gezeigt ist relativ komplex und der Aufwand wird von vielen Entwicklern gescheut. Mit Generatoren lassen sich Iteratoren auf einfache Weise erstellen und sie machen damit die Einsparung von Ressourcen einfach zugänglich. Ein Generator lässt sich auf zwei verschiedene Arten erzeugen, zum einen mit Generator-Expressions und zum anderen mit Generator-Funktionen.Generator-Expression
Einfache Generatoren lassen sich mit Generator-Expressions implementieren. Sie haben, wie im zweiten Beispiel ersichtlich, eine an List-Comprehensions angelehnte Syntax. Ähnlich wie lambda anonyme Funktionen erstellt, erstellen Generator-Expressions anonyme Generator-Funktionen. In der Regel werden die Expressions in runden Klammern geschrieben. Diese können, falls die Expression das einzige Argument einer Funktion ist, weggelassen werden. Im Gegensatz zu einer Liste wird ein Generator „verbraucht", man kann z. B. nur einmal über ihn iterieren (siehe Abbildung 3). Ebenso kann auf einen Generator nicht über Sequence-Operationen zugegriffen werden. Wird also eine Liste, die mittels List-Comprehension generiert wurde, nicht häufiger benötigt bzw. es müssen keine speziellen Methoden oder Sequence-Operationen auf sie ausgeführt werden, sollte sie durch eine Generator-Expression ersetzt werden.Generator-Funktionen
Es lassen sich mit Generator-Expressions jedoch nicht beliebig komplizierte Generatoren erstellen. An dieser Stelle kommen Generator-Funktionen ins Spiel. Die in den oberen Beispielen dargestellte Expression könnte als Funktion wie folgt definiert werden.
Pipelining mit Generatoren
Mit den bisher genannten Beispielen lässt sich zwar das Verhalten von Generatoren gut erklären, aber wirkliche Begeisterung lässt sich damit schwer hervorrufen. Leider hören die Erläuterungen in vielen Python-Büchern an dieser Stelle auf. Dies erklärt eventuell den relativ seltenen Einsatz von Generatoren in Python-Programmen. Folgendes Beispiel zeigt, wie mithilfe von Generator-Expressions und -Funktionen alle Apache-Access-Logfiles in einem Verzeichnis verarbeitet werden können, um eine Liste aller IPs, über die auf den Webserver zugegriffen wurde, zu erstellen (siehe Abbildung 4).
Natürlich fehlt in dem Beispiel das gesamte Exception-Handling, aber es ist auf den ersten Blick ersichtlich, dass der Code durch die Nutzung von Generatoren deutlich eleganter wird. Folgendermaßen läuft die Verarbeitung ab:
- Der Generator-Funktion open_logfiles() wird ein Pfad und ein RegEx-Muster übergeben. Er öffnet die entsprechenden Dateien, gibt File-Objekte zurück und nutzt, falls es sich um eine gepackte Datei handelt, die open()Funktion des entsprechenden Moduls.
- gen_lines() iteriert über die File-Objekte und gibt die einzelnen Zeilen der Dateien zurück.
- Diese werden in der Generator-Expression verarbeitet, die jeweils die IP-Adresse zurückgibt.
Die genutzte Verknüpfung von Generatoren hat David M. Beazley schon 2008 auf der PyCon als Pipelining beschrieben. Seine Talks zu dem Thema sind die de-facto-Referenz und können als weiterführende Lektüre dienen. Dort erläutert er z. B. auch, wie Co-Routinen auf Basis von Generatoren implementiert werden.