Python Generators
Table of Contents
In diesem Tutorial erfahren Sie, wie Sie mit Python Generators auf einfache Weise Iterationen erstellen, wie sie sich von Iteratoren und normalen Funktionen unterscheiden und warum Sie sie verwenden sollten.
Generators in Python
Es steckt viel Arbeit im Bau und iterator in Python. Wir müssen eine Klasse implementieren mit __iter__()
und __next__()
Methode, verfolgen die internen Zustände und lösen StopIteration
wenn keine Werte zurückgegeben werden sollen.
Dies ist sowohl langwierig als auch kontraintuitiv. In solchen Situationen kommt Generator zur Rettung.
Python Generators sind eine einfache Möglichkeit, Iterators zu erstellen. All die oben erwähnten Arbeiten werden in Python automatisch von Generatoren erledigt.
Einfach gesagt ist ein Generator eine Funktion, die ein Objekt (Iterator) zurückgibt, über das wir iterieren können (einen Wert nach dem anderen).
Create Generators in Python
Es ist ziemlich einfach, einen Generator in Python zu erstellen. Es ist so einfach wie das Definieren einer normalen Funktion, aber mit einer yield
statement anstelle einer return
statement.
Enthält eine Funktion mindestens einen yield
statement ((sie kann andere yield
oder return
statements)wird sie zu einer Generator funktion. Beide yield
und return
gibt einen Wert aus einer Funktion zurück.
Der Unterschied besteht darin, dass bei einem return
statement beendet eine Funktion vollständig, yield
hält die Funktion an, indem sie alle Zustände speichert und später bei weiteren Aufrufen von dort aus fortfährt.
Differences between Generator function and Normal function
Hier ist der Unterschied zwischen einer Generator funktion und einer normalen Funktion.
- Generator funktion enthält eine oder mehrere
yield
statements. - Wenn es aufgerufen wird, gibt es ein Objekt (Iterator) zurück, startet jedoch nicht sofort die Ausführung.
- Methoden wie
__iter__()
and__next__()
werden automatisch implementiert. So können wir die Elemente mit . iterierennext()
. - Sobald die Funktion nachgibt, wird die Funktion angehalten und die Kontrolle an den Aufrufer übergeben.
- Lokale Variablen und ihre Zustände werden zwischen aufeinanderfolgenden Aufrufen gespeichert.
- Schließlich, wenn die Funktion beendet ist
StopIteration
wird bei weiteren Anrufen automatisch ausgelöst.
Hier ist ein Beispiel, um alle oben genannten Punkte zu veranschaulichen. Wir haben eine Generator funktion namens my_gen()
mit mehreren yield
statements.
# A simple generator function def my_gen(): n = 1 print('Dies wird zuerst gedruckt') # Generator Funktion enthält Yield-Anweisungen statements yield n n += 1 print('Dies ist die zweite gedruckte') yield n n += 1 print('Dies wird endlich gedruckt') yield n
Ein interaktiver Ablauf im Interpreter ist unten angegeben. Führen Sie diese in der Python-Shell aus, um die Ausgabe anzuzeigen.
>>> # Es gibt ein Objekt zurück, startet jedoch nicht sofort die Ausführung. >>> a = my_gen() >>> # Wir können durch die Elemente iterieren mit next(). >>> next(a) Dies wird zuerst gedruckt 1 >>> # Sobald die Funktion nachgibt, wird die Funktion angehalten und die Kontrolle an den Aufrufer übergeben. >>> # Lokale Variablen und ihre Zustände werden zwischen aufeinanderfolgenden Aufrufen gespeichert. >>> next(a) Dies ist die zweite gedruckte 2 >>> next(a) This is printed at last 3 >>> # Wenn die Funktion schließlich beendet wird, wird StopIteration bei weiteren Aufrufen automatisch ausgelöst. >>> next(a) Traceback (most recent call last): ... StopIteration >>> next(a) Traceback (most recent call last): ... StopIteration
Interessant im obigen Beispiel ist, dass der Wert der Variablen n wird zwischen den einzelnen Aufrufen gespeichert.
Im Gegensatz zu normalen Funktionen werden die lokalen Variablen nicht zerstört, wenn die Funktion nachgibt. Außerdem kann das Generator objekt nur einmal iteriert werden.
Um den Prozess neu zu starten, müssen wir ein weiteres Generator objekt erstellen, indem wir etwas wie a = my_gen()
.
Eine letzte Sache, die zu beachten ist, ist, dass wir Generatoren direkt mit for-Schleifen verwenden können.
Dies liegt daran, dass ein for
Schleife nimmt einen Iterator und iteriert über ihn mit next()
Funktion. Sie endet automatisch, wenn StopIteration
ausgelöst wird. Sehen Sie hier nach, wie eine for-Schleife in Python tatsächlich implementiert ist.
# Eine einfache Generator funktion def my_gen(): n = 1 print('Dies wird zuerst gedruckt') # Generator funktion enthält yield statements yield n n += 1 print('Dies ist die zweite gedruckte') yield n n += 1 print('Dies wird endlich gedruckt') yield n # Using for loop for item in my_gen(): print(item)
Wenn Sie das Programm ausführen, wird die Ausgabe sein
Dies wird zuerst gedruckt 1 Dies ist die zweite gedruckte 2 Dies wird endlich gedruckt 3
Python Generators with a Loop
Das obige Beispiel ist von geringerem Nutzen und wir haben es nur untersucht, um eine Vorstellung davon zu bekommen, was im Hintergrund passiert.
Normalerweise werden Generator funktionen mit einer Schleife implementiert, die eine geeignete Abbruchbedingung hat.
Nehmen wir ein Beispiel für einen Generator, der eine Zeichenkette 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)
Output
o l l e h
In diesem Beispiel haben wir die range()
Funktion, um den Index in umgekehrter Reihenfolge mit Hilfe der for-Schleife zu erhalten.
Notiz
Diese Generator funktion arbeitet nicht nur mit Strings, sondern auch mit anderen Arten von Iterablen wie List, Tupel, etc.
Python Generator Expression
Einfache Generators können mit Hilfe von Generator ausdrücken einfach on-the-fly erstellt werden. Es macht das Erstellen von Generators einfach.
Einfache Generatoren können mit Hilfe von Generatorausdrücken einfach on-the-fly erstellt werden. Es macht das Erstellen von Generatoren einfach.
Ähnlich wie die Lambda-Funktionen, die anonyme Funktionen erzeugen, erzeugen Generatorausdrücke anonyme Generatorfunktionen.
Die Syntax für Generatorausdrücke ist ähnlich wie die einer Listenauffassung in Python. Allerdings werden die eckigen Klammern durch runde Klammern ersetzt.
Der Hauptunterschied zwischen einem Listenaufruf und einem Generatorausdruck ist, dass ein Listenaufruf die gesamte Liste erzeugt, während der Generatorausdruck jeweils ein Element erzeugt.
Sie haben eine träge Ausführung (produzieren Elemente nur, wenn sie angefordert werden). Aus diesem Grund ist ein Generatorausdruck viel speichereffizienter als ein entsprechendes Listenverständnis.
# Initialisieren der Liste my_list = [1, 3, 6, 10] # jeden Begriff mit Hilfe des Listenverständnisses quadrieren list_ = [x**2 for x in my_list] # Das Gleiche kann mit einem Generator-Ausdruck gemacht werden # Generatorausdrücke sind von Klammern umgeben () generator = (x**2 for x in my_list) print(list_) print(generator)
Output
[1, 9, 36, 100] <generator object <genexpr> at 0x7f5d4eb4bf50>
Wir können oben sehen, dass der Generator-Ausdruck nicht sofort das gewünschte Ergebnis erzeugt hat. Stattdessen wurde ein Generatorobjekt zurückgegeben, das Elemente nur bei Bedarf produziert.
Hier sehen Sie, wie wir anfangen können, Elemente vom Generator abzurufen:
# 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)
Wenn wir das obige Programm ausführen, erhalten wir die folgende Ausgabe:
1 9 36 100 Traceback (most recent call last): File "<string>", line 15, in <module> StopIteration
Generator ausdrücke können als Funktionsargumente verwendet werden. Bei einer solchen Verwendung können die runden Klammern entfallen.
>>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100
Use of Python Generators
Es gibt mehrere Gründe, die Generators zu einer leistungsfähigen Implementierung machen.
1. Einfach zu implementieren
Generatorslassen sich im Vergleich zu ihrem Pendant in der Iterator-Klasse übersichtlich und klar implementieren. Es folgt ein Beispiel für die Implementierung einer Folge von 2er-Potenzen mit einer Iterator-Klasse.
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
Das obige Programm war langwierig und verwirrend. Lassen Sie uns nun das Gleiche mit einer Generatorfunktion machen.
def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1
Da die Generators die Details automatisch verfolgen, war die Implementierung übersichtlich und viel sauberer.
2. Memory Efficient
Eine normale Funktion zur Rückgabe einer Sequenz erstellt die gesamte Sequenz im Speicher, bevor sie das Ergebnis zurückgibt. Dies ist ein Overkill, wenn die Anzahl der Elemente in der Sequenz sehr groß ist.
Die Generator implementierung solcher Sequenzen ist speicherfreundlich und wird bevorzugt, da sie jeweils nur ein Element erzeugt.
3. Represent Infinite Stream
Generatoren sind ein hervorragendes Medium zur Darstellung eines unendlichen Datenstroms. Unendliche Datenströme können nicht im Speicher gespeichert werden, und da Generatoren jeweils nur ein Element erzeugen, können sie einen unendlichen Datenstrom darstellen.
Die folgende Generator funktion kann alle geraden Zahlen erzeugen (zumindest theoretisch).
def all_even(): n = 0 while True: yield n n += 2
4. Pipelining Generators
Mehrere Generatorskönnen verwendet werden, um eine Reihe von Operationen über eine Pipeline zu leiten. Dies lässt sich am besten anhand eines Beispiels veranschaulichen.
Angenommen, wir haben einen Generator, der die Zahlen der Fibonacci-Reihe erzeugt. Und wir haben einen weiteren Generator zum Quadrieren von Zahlen.
Wenn wir die Summe der Quadrate der Zahlen in der Fibonacci-Reihe herausfinden wollen, können wir dies auf folgende Weise tun, indem wir die Ausgabe der Generator funktionen zusammen pipelining.
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))))
Output
4895
Dieses Pipelining ist effizient und einfach zu lesen.