In diesem Tutorial erfahren Sie, wie Sie mit Python-Generatoren einfach Iterationen erstellen, wie sie sich von Iteratoren und normalen Funktionen unterscheiden und warum Sie sie verwenden sollten.
Video: Python-Generatoren
Generatoren in Python
Das Erstellen eines Iterators in Python erfordert viel Arbeit. Wir müssen eine Klasse mit __iter__()
und __next__()
method implementieren , interne Zustände verfolgen und erhöhen, StopIteration
wenn keine Werte zurückgegeben werden müssen.
Dies ist sowohl langwierig als auch nicht intuitiv. Generator kommt in solchen Situationen zur Rettung.
Python-Generatoren sind eine einfache Möglichkeit, Iteratoren zu erstellen. Alle oben genannten Arbeiten werden automatisch von Generatoren in Python ausgeführt.
Einfach ausgedrückt ist ein Generator eine Funktion, die ein Objekt (Iterator) zurückgibt, über das wir iterieren können (jeweils einen Wert).
Generatoren in Python erstellen
Es ist ziemlich einfach, einen Generator in Python zu erstellen. Es ist so einfach wie das Definieren einer normalen Funktion, jedoch mit einer yield
Anweisung anstelle einer return
Anweisung.
Wenn eine Funktion mindestens eine yield
Anweisung enthält (sie kann andere yield
oder return
Anweisungen enthalten), wird sie zu einer Generatorfunktion. Beide yield
und return
geben einen Wert von einer Funktion zurück.
Der Unterschied besteht darin, dass während eine return
Anweisung eine Funktion vollständig beendet, die yield
Anweisung die Funktion pausiert, alle ihre Zustände speichert und später bei aufeinanderfolgenden Aufrufen fortfährt.
Unterschiede zwischen Generatorfunktion und Normalfunktion
So unterscheidet sich eine Generatorfunktion von einer normalen Funktion.
- Die Generatorfunktion enthält eine oder mehrere
yield
Anweisungen. - Beim Aufruf wird ein Objekt (Iterator) zurückgegeben, die Ausführung wird jedoch nicht sofort gestartet.
- Methoden wie
__iter__()
und__next__()
werden automatisch implementiert. So können wir die Elemente mit durchlaufennext()
. - Sobald die Funktion nachgibt, wird die Funktion angehalten und die Steuerung an den Anrufer übertragen.
- Lokale Variablen und ihre Zustände werden zwischen aufeinanderfolgenden Aufrufen gespeichert.
- Wenn die Funktion beendet wird,
StopIteration
wird sie bei weiteren Aufrufen automatisch ausgelöst.
Hier ist ein Beispiel, um alle oben genannten Punkte zu veranschaulichen. Wir haben eine Generatorfunktion my_gen()
mit mehreren yield
Anweisungen.
# A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n
Ein interaktiver Lauf im Interpreter ist unten angegeben. Führen Sie diese in der Python-Shell aus, um die Ausgabe anzuzeigen.
>>> # It returns an object but does not start execution immediately. >>> a = my_gen() >>> # We can iterate through the items using next(). >>> next(a) This is printed first 1 >>> # Once the function yields, the function is paused and the control is transferred to the caller. >>> # Local variables and theirs states are remembered between successive calls. >>> next(a) This is printed second 2 >>> next(a) This is printed at last 3 >>> # Finally, when the function terminates, StopIteration is raised automatically on further calls. >>> next(a) Traceback (most recent call last):… StopIteration >>> next(a) Traceback (most recent call last):… StopIteration
Eine interessante Sache, die im obigen Beispiel zu beachten ist, ist, dass der Wert der Variablen n zwischen jedem Aufruf gespeichert wird.
Im Gegensatz zu normalen Funktionen werden die lokalen Variablen nicht zerstört, wenn die Funktion nachgibt. Darüber hinaus kann das Generatorobjekt nur einmal iteriert werden.
Um den Prozess neu zu starten, müssen wir ein anderes Generatorobjekt mit so etwas wie erstellen a = my_gen()
.
Eine letzte Sache zu beachten ist, dass wir Generatoren mit for-Schleifen direkt verwenden können.
Dies liegt daran, dass eine for
Schleife einen Iterator verwendet und ihn mithilfe der next()
Funktion durchläuft . Es endet automatisch, wenn StopIteration
es angehoben wird. Überprüfen Sie hier, wie eine for-Schleife tatsächlich in Python implementiert ist.
# A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n # Using for loop for item in my_gen(): print(item)
Wenn Sie das Programm ausführen, lautet die Ausgabe wie folgt:
Dies wird zuerst gedruckt 1 Dies wird als zweites gedruckt 2 Dies wird zuletzt gedruckt 3
Python-Generatoren mit einer Schleife
Das obige Beispiel ist weniger nützlich und wir haben es untersucht, um eine Vorstellung davon zu bekommen, was im Hintergrund geschah.
Normalerweise werden Generatorfunktionen mit einer Schleife implementiert, die eine geeignete Abschlussbedingung aufweist.
Nehmen wir ein Beispiel eines Generators, der eine Zeichenfolge umkehrt.
def rev_str(my_str): length = len(my_str) for i in range(length - 1, -1, -1): yield my_str(i) # For loop to reverse the string for char in rev_str("hello"): print(char)
Ausgabe
olleh
In diesem Beispiel haben wir die range()
Funktion verwendet, um den Index mithilfe der for-Schleife in umgekehrter Reihenfolge abzurufen.
Hinweis : Diese Generatorfunktion funktioniert nicht nur mit Zeichenfolgen, sondern auch mit anderen Arten von Iterablen wie Liste, Tupel usw.
Python-Generator-Ausdruck
Einfache Generatoren können mithilfe von Generatorausdrücken einfach im laufenden Betrieb erstellt werden. Es macht den Bau von Generatoren einfach.
Ähnlich wie die Lambda-Funktionen, die anonyme Funktionen erstellen, erstellen Generatorausdrücke anonyme Generatorfunktionen.
Die Syntax für den Generatorausdruck ähnelt der eines Listenverständnisses in Python. Die eckigen Klammern werden jedoch durch runde Klammern ersetzt.
Der Hauptunterschied zwischen einem Listenverständnis und einem Generatorausdruck besteht darin, dass ein Listenverständnis die gesamte Liste erzeugt, während der Generatorausdruck jeweils ein Element erzeugt.
They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.
# Initialize the list my_list = (1, 3, 6, 10) # square each term using list comprehension list_ = (x**2 for x in my_list) # same thing can be done using a generator expression # generator expressions are surrounded by parenthesis () generator = (x**2 for x in my_list) print(list_) print(generator)
Output
(1, 9, 36, 100)
We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.
Here is how we can start getting items from the generator:
# Initialize the list my_list = (1, 3, 6, 10) a = (x**2 for x in my_list) print(next(a)) print(next(a)) print(next(a)) print(next(a)) next(a)
When we run the above program, we get the following output:
1 9 36 100 Traceback (most recent call last): File "", line 15, in StopIteration
Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.
>>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100
Use of Python Generators
There are several reasons that make generators a powerful implementation.
1. Easy to Implement
Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.
class PowTwo: def __init__(self, max=0): self.n = 0 self.max = max def __iter__(self): return self def __next__(self): if self.n> self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result
The above program was lengthy and confusing. Now, let's do the same using a generator function.
def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1
Since generators keep track of details automatically, the implementation was concise and much cleaner.
2. Memory Efficient
A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.
Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.
3. Represent Infinite Stream
Generatoren sind ausgezeichnete Medien, um einen unendlichen Datenstrom darzustellen. Unendliche Streams können nicht im Speicher gespeichert werden. Da Generatoren jeweils nur ein Element produzieren, können sie einen unendlichen Datenstrom darstellen.
Die folgende Generatorfunktion kann (zumindest theoretisch) alle geraden Zahlen erzeugen.
def all_even(): n = 0 while True: yield n n += 2
4. Pipelining-Generatoren
Mehrere Generatoren können verwendet werden, um eine Reihe von Operationen zu leiten. Dies lässt sich am besten anhand eines Beispiels veranschaulichen.
Angenommen, wir haben einen Generator, der die Zahlen in der Fibonacci-Reihe erzeugt. Und wir haben einen weiteren Generator zum Quadrieren von Zahlen.
Wenn wir die Summe der Quadrate von Zahlen in der Fibonacci-Reihe herausfinden wollen, können wir dies auf folgende Weise tun, indem wir die Ausgabe von Generatorfunktionen zusammenpipeln.
def fibonacci_numbers(nums): x, y = 0, 1 for _ in range(nums): x, y = y, x+y yield x def square(nums): for num in nums: yield num**2 print(sum(square(fibonacci_numbers(10))))
Ausgabe
4895
Dieses Pipelining ist effizient und leicht zu lesen (und ja, viel cooler!).