Die Generator-Expression in Python

Generator-Expression erstellen

Hier haben wir eine Liste und eine list comprehension, die eine Schleife über diese Liste ausführt:

>>> numbers = [2, 1, 3, 4, 7, 11, 18]
>>> squares = [n**2 for n in numbers]

Wenn wir die eckigen Klammern [ und ] in dieser list comprehension in runde Klammern ( und ) umwandeln, wird unsere list comprehension zu eine generator expression.

>>> squares = (n**2 for n in numbers)

list comprehensions geben neue Listen zurück. Generatorausdrücke geben neue Generator-Objekte zurück.

>>> squares
<generator object <genexpr> at 0x7fcb363347b0>

Ein Generator-Objekt hat, anders als eine Liste, keine Länge!

>>> len(squares)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: object of type 'generator' has no len()

Wenn wir versuchen, ein Generator-Objekt zu indizieren, um zum Beispiel sein erstes Element zu erhalten, erhalten wir einen Fehler:

>>> squares[0]
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: 'generator' object is not subscriptable

Man kann einen Generator nicht indizieren!

Das Einzige, was wir mit einem Generator wirklich tun können, ist, eine Schleife über ihn durchlaufen zu lassen:

>>> for n in squares:
... print(n)
...
4
1
9
16
49
121
324

Es scheint, dass Generatoren weniger Funktionen haben als Listen. Warum sollten wir also überhaupt einen Generator-Ausdruck verwenden wollen?

Warum Generatoren verwenden?

Der Vorteil von Generatoren ist, dass sie „Lazy Iterables“ sind, d. h. sie arbeiten erst, wenn man eine Schleife über sie durchläuft.
Direkt nach der Auswertung eines Generatorausdrucks wird ein Generator-Objekt erzeugt:

>>> squares = (n**2 for n in numbers)
>>> squares
<generator object <genexpr> at 0x7fd49a500900>

Aber bis zu diesem Punkt hat dieser Generator eigentlich nichts berechnet. Er enthält keine Werte, im Gegensatz zu einer Liste.

Wenn wir also die Zahl 4 in unserer Liste (bei Index 3) in die Zahl 5 ändern:

>>> numbers
[2, 1, 3, 4, 7, 11, 18]
>>> numbers[3] = 5
>>> numbers
[2, 1, 3, 5, 7, 11, 18]

Wenn wir dann eine Schleife über unser Generator-Objekt durchlaufenn (mit einem Listenkonstruktor, einer for-Schleife oder einer anderen Form der Schleifenbildung), werden wir sehen, dass das vierte Element nicht 16, sondern 25 ist:

>>> list(squares)
[4, 1, 9, 25, 49, 121, 324]

Generatoren arbeiten erst dann, wenn sie in einer Schleife durchlaufen werden.

Und wenn man einen Generator ein zweites Mal durchläuft, ist er leer:

>>> list(squares)
[]

Generator-Objekte sind „Lazy Iterables“, also Iterables zur einmaligen Verwendung. Elemente werden in einer Schleife über einen Generator erzeugt (das macht sie träge) und diese Elemente werden in einer Schleife über den Generator verbraucht, d.h. sie werden nirgendwo gespeichert (das macht sie zu Einwegobjekten).

Schleife über einen Teil des Generators

Wenn alle Elemente eines Generators verbraucht wurden (d. h. wir haben eine vollständige Schleife über ihn gezogen), sagen wir, dass er erschöpft ist. Der obige Quadratzahlengenerator war erschöpft:

>>> list(squares)
[]

Man muss die Generatoren nicht unbedingt vollständig ausschöpfen, wenn man sie in einer Schleife durchläuft. Man kann eine Schleife über einen Generator beginnen und dann anhalten, sobald eine Bedingung erfüllt ist (n > 10 im Beispiel):

>>> numbers = [2, 1, 3, 4, 7, 11, 18]
>>> squares = (n**2 for n in numbers)
>>> for n in squares:
... print(n)
... if n > 10:
... break
...
4
1
9
16

Wenn man dann die Schleife erneut starten würde (in diesem Fall mit dem Listenkonstruktor), würde der Generator dort beginnen, wo er vorher aufgehört hat:

>>> list(squares)
[49, 121, 324]

Generatoren erzeugen Werte, wenn man eine Schleife über sie durchläuft.

Das Einzige, was man mit einem Generatorobjekt tun kann, ist, eine Schleife über ihn zu durchlaufen. Sobald man eine Schleife über ein Generatorobjekt durhclaufen hat (d.h. es wurde erschöpft, indem man alle darin enthaltenen Elemente verbraucht hat), hat es keinen wirklichen Nutzen mehr. Sobald ein Generator erschöpft ist, ist er für immer leer.

Nur das nächste Element generieren

Es gibt noch eine weitere Möglichkeit, mit den Generatoren umzugehen (abgesehen von der Schleifenbildung), auch wenn sie etwas ungewöhnlich ist. Alle Generatoren können an die built-in Funktion next übergeben werden.

Die next-Funktion liefert uns das nächste Element in einem Generator:

>>> numbers = [2, 1, 3, 4, 7, 11, 18]
>>> squares = (n**2 for n in numbers)
>>> next(squares)
4

Generatoren behalten den Ausdruck im Auge, den sie für die Iterable, über die sie eine Schleife ziehen, auswerten müssen, und sie behalten im Auge, wo sie sich in der Iterable befinden.

Wenn wir next bei einem Generator wiederholt aufrufen, erhalten wir jedes einzelne Element des Generators:

>>> next(squares)
1
>>> next(squares)
9
>>> next(squares)
16
>>> next(squares)
49
>>> next(squares)
121
>>> next(squares)
324

Wenn wir next bei einem Generator aufrufen, der erschöpft ist (er wurde vollständig verbraucht), erhalten wir eine StopIteration-Exception:

>>> next(squares)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Die StopIteration-Exception zeigt an, dass dieser Generator keine Werte mehr enthält (er ist leer):

>>> list(squares)
[]

Zusammenfassung

Genauso wie list comprehensions neue Listen erzeugen, erzeugen Generatorausdrücke neue Generatorobjekte.

Ein Generator ist eine Iterable, die eigentlich keine Werte enthält oder speichert; sie erzeugt Werte, wenn man eine Schleife darüber laufen lässt.

Das bedeutet, dass Generatoren speichereffizienter sind als Listen, da sie nicht wirklich Speicher für ihre Werte benötigen. Stattdessen generieren sie die Werte in der Schleife, während man sie durchläuft.

Mit Generatorausdrücken erhalten wir Generatoren, die faule Iterables zur einmaligen Verwendung sind.