Hinweis: In diesem Beitrag wird die Python 3.x-Syntax angenommen. †
Ein Generator ist einfach eine Funktion, die ein Objekt zurückgibt, das Sie aufrufen können next
, sodass bei jedem Aufruf ein Wert zurückgegeben wird, bis eine StopIteration
Ausnahme ausgelöst wird, die signalisiert, dass alle Werte generiert wurden. Ein solches Objekt wird als Iterator bezeichnet .
Normale Funktionen geben einen einzelnen Wert mit zurück return
, genau wie in Java. In Python gibt es jedoch eine Alternative namens yield
. Die Verwendung einer yield
beliebigen Stelle in einer Funktion macht sie zu einem Generator. Beachten Sie diesen Code:
>>> def myGen(n):
... yield n
... yield n + 1
...
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Wie Sie sehen können, myGen(n)
ist eine Funktion, die ergibt n
und n + 1
. Jeder Aufruf von next
ergibt einen einzelnen Wert, bis alle Werte erhalten wurden. for
Schleifen rufen next
im Hintergrund auf, also:
>>> for n in myGen(6):
... print(n)
...
6
7
Ebenso gibt es Generatorausdrücke , mit denen bestimmte gängige Generatortypen kurz und bündig beschrieben werden können:
>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Beachten Sie, dass Generatorausdrücke dem Listenverständnis sehr ähnlich sind :
>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
Beachten Sie, dass ein Generatorobjekt einmal generiert wird, sein Code jedoch nicht auf einmal ausgeführt wird. Nur Aufrufe, next
um den Code tatsächlich (teilweise) auszuführen. Die Ausführung des Codes in einem Generator stoppt, sobald eine yield
Anweisung erreicht wurde, bei der ein Wert zurückgegeben wird. Der nächste Aufruf von next
bewirkt dann, dass die Ausführung in dem Zustand fortgesetzt wird, in dem der Generator nach dem letzten verlassen wurde yield
. Dies ist ein grundlegender Unterschied zu regulären Funktionen: Diese beginnen immer mit der Ausführung "oben" und verwerfen ihren Status, wenn sie einen Wert zurückgeben.
Zu diesem Thema gibt es noch mehr zu sagen. Es ist zB möglich, send
Daten in einen Generator ( Referenz ) zurückzusenden. Ich schlage jedoch vor, dass Sie sich erst damit befassen, wenn Sie das Grundkonzept eines Generators verstanden haben.
Nun fragen Sie sich vielleicht: Warum Generatoren verwenden? Es gibt mehrere gute Gründe:
- Bestimmte Konzepte können mit Generatoren viel prägnanter beschrieben werden.
- Anstatt eine Funktion zu erstellen, die eine Liste von Werten zurückgibt, kann man einen Generator schreiben, der die Werte im laufenden Betrieb generiert. Dies bedeutet, dass keine Liste erstellt werden muss, was bedeutet, dass der resultierende Code speichereffizienter ist. Auf diese Weise kann man sogar Datenströme beschreiben, die einfach zu groß wären, um in den Speicher zu passen.
Generatoren ermöglichen eine natürliche Beschreibung unendlicher Ströme. Betrachten Sie zum Beispiel die Fibonacci-Zahlen :
>>> def fib():
... a, b = 0, 1
... while True:
... yield a
... a, b = b, a + b
...
>>> import itertools
>>> list(itertools.islice(fib(), 10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Dieser Code verwendet itertools.islice
, um eine endliche Anzahl von Elementen aus einem unendlichen Strom zu entnehmen. Es wird empfohlen, sich die Funktionen im itertools
Modul genau anzusehen , da sie wichtige Werkzeuge für das einfache Schreiben fortschrittlicher Generatoren sind.
† Über Python <= 2.6: In den obigen Beispielen next
ist eine Funktion, die die Methode __next__
für das angegebene Objekt aufruft . In Python <= 2.6 verwendet man eine etwas andere Technik, nämlich o.next()
anstelle von next(o)
. Python 2.7 hat einen next()
Aufruf, .next
sodass Sie in 2.7 nicht Folgendes verwenden müssen:
>>> g = (n for n in range(3, 5))
>>> g.next()
3
send
Daten an einen Generator zu senden. Sobald Sie das tun, haben Sie eine "Coroutine". Es ist sehr einfach, Muster wie das erwähnte Consumer / Producer mit Coroutinen zu implementieren, da sie keinLock
s benötigen und daher nicht blockieren können. Es ist schwer, Coroutinen zu beschreiben, ohne Fäden zu schlagen, daher sage ich nur, dass Koroutinen eine sehr elegante Alternative zum Einfädeln sind.Ein Generator ist effektiv eine Funktion, die (Daten) zurückgibt, bevor sie beendet ist. Sie wird jedoch an diesem Punkt angehalten, und Sie können die Funktion an diesem Punkt wieder aufnehmen.
und so weiter. Der (oder ein) Vorteil von Generatoren besteht darin, dass Sie große Datenmengen verarbeiten können, da sie Daten einzeln verarbeiten. Bei Listen kann ein übermäßiger Speicherbedarf zum Problem werden. Generatoren sind genau wie Listen iterierbar, sodass sie auf die gleiche Weise verwendet werden können:
Beachten Sie, dass Generatoren beispielsweise eine andere Möglichkeit bieten, mit Unendlichkeit umzugehen
Der Generator kapselt eine Endlosschleife, aber dies ist kein Problem, da Sie jede Antwort nur jedes Mal erhalten, wenn Sie danach fragen.
quelle
Erstens war der Begriff Generator in Python ursprünglich etwas schlecht definiert, was zu viel Verwirrung führte. Sie meinen wahrscheinlich Iteratoren und Iterables (siehe hier ). Dann gibt es in Python auch Generatorfunktionen (die ein Generatorobjekt zurückgeben), Generatorobjekte (die Iteratoren sind) und Generatorausdrücke (die zu einem Generatorobjekt ausgewertet werden).
Laut dem Glossareintrag für Generator scheint es, dass die offizielle Terminologie jetzt lautet, dass Generator für "Generatorfunktion" steht. In der Vergangenheit wurden die Begriffe in der Dokumentation inkonsistent definiert, aber zum Glück wurde dies behoben.
Es könnte immer noch eine gute Idee sein, präzise zu sein und den Begriff "Generator" ohne weitere Spezifikation zu vermeiden.
quelle
Generatoren können als Abkürzung für die Erstellung eines Iterators angesehen werden. Sie verhalten sich wie ein Java-Iterator. Beispiel:
Hoffe das hilft / ist was du suchst.
Aktualisieren:
Wie viele andere Antworten zeigen, gibt es verschiedene Möglichkeiten, einen Generator zu erstellen. Sie können die Klammernsyntax wie in meinem obigen Beispiel verwenden oder die Ausbeute verwenden. Ein weiteres interessantes Merkmal ist, dass Generatoren "unendlich" sein können - Iteratoren, die nicht aufhören:
quelle
Stream
s, die Generatoren viel ähnlicher sind, außer dass Sie anscheinend nicht einfach das nächste Element ohne überraschend viel Aufwand erhalten können.Es gibt kein Java-Äquivalent.
Hier ist ein erfundenes Beispiel:
Es gibt eine Schleife im Generator, die von 0 bis n läuft, und wenn die Schleifenvariable ein Vielfaches von 3 ist, ergibt sie die Variable.
Während jeder Iteration der
for
Schleife wird der Generator ausgeführt. Wenn der Generator zum ersten Mal ausgeführt wird, startet er am Anfang, andernfalls wird er vom vorherigen Zeitpunkt an fortgesetzt.quelle
print "hello"
nachdem diex=x+1
in meinem Beispiel „Hallo“ würde 100 Mal gedruckt werden, während der Körper der for - Schleife würde immer noch nur 33 - mal ausgeführt werden.Ich beschreibe Generatoren gerne mit Stack-Frames für diejenigen mit einem anständigen Hintergrund in Programmiersprachen und Computer.
In vielen Sprachen gibt es einen Stapel, auf dem sich der aktuelle Stapel "Frame" befindet. Der Stapelrahmen enthält Speicherplatz für Variablen, die für die Funktion lokal sind, einschließlich der an diese Funktion übergebenen Argumente.
Wenn Sie eine Funktion aufrufen, wird der aktuelle Ausführungspunkt (der "Programmzähler" oder ein gleichwertiger Punkt) auf den Stapel verschoben und ein neuer Stapelrahmen erstellt. Die Ausführung wird dann an den Anfang der aufgerufenen Funktion übertragen.
Bei regulären Funktionen gibt die Funktion irgendwann einen Wert zurück und der Stapel wird "gepoppt". Der Stapelrahmen der Funktion wird verworfen und die Ausführung am vorherigen Speicherort fortgesetzt.
Wenn eine Funktion ein Generator ist, kann sie mithilfe der Yield-Anweisung einen Wert zurückgeben, ohne dass der Stapelrahmen verworfen wird. Die Werte der lokalen Variablen und des Programmzählers innerhalb der Funktion bleiben erhalten. Auf diese Weise kann der Generator zu einem späteren Zeitpunkt wieder aufgenommen werden, wobei die Ausführung von der Yield-Anweisung aus fortgesetzt wird. Außerdem kann mehr Code ausgeführt und ein anderer Wert zurückgegeben werden.
Vor Python 2.5 waren dies alle Generatoren. Python 2.5 hinzugefügt , um die Fähigkeit , Werte zu überschreiten zurück in als auch an den Generator. Dabei steht der übergebene Wert als Ausdruck zur Verfügung, der sich aus der Yield-Anweisung ergibt, die vorübergehend die Kontrolle (und einen Wert) vom Generator zurückgegeben hat.
Der Hauptvorteil für Generatoren besteht darin, dass der "Status" der Funktion erhalten bleibt, im Gegensatz zu regulären Funktionen, bei denen jedes Mal, wenn der Stapelrahmen verworfen wird, der gesamte "Status" verloren geht. Ein sekundärer Vorteil besteht darin, dass ein Teil des Overheads für Funktionsaufrufe (Erstellen und Löschen von Stapelrahmen) vermieden wird, obwohl dies normalerweise ein kleiner Vorteil ist.
quelle
Das einzige, was ich zu Stephan202s Antwort hinzufügen kann, ist eine Empfehlung, dass Sie sich David Beazleys PyCon '08 -Präsentation "Generator Tricks for Systems Programmers" ansehen, die die beste Erklärung für das Wie und Warum von Generatoren ist, die ich gesehen habe irgendwo. Dies ist die Sache, die mich von "Python sieht irgendwie lustig aus" zu "Das ist, wonach ich gesucht habe" geführt hat. Es ist bei http://www.dabeaz.com/generators/ .
quelle
Es hilft, klar zwischen der Funktion foo und dem Generator foo (n) zu unterscheiden:
foo ist eine Funktion. foo (6) ist ein Generatorobjekt.
Die typische Art, ein Generatorobjekt zu verwenden, ist eine Schleife:
Die Schleife wird gedruckt
Stellen Sie sich einen Generator als eine wiederaufnehmbare Funktion vor.
yield
verhält sich wiereturn
in dem Sinne, dass Werte, die sich ergeben, vom Generator "zurückgegeben" werden. Im Gegensatz zur Rückgabe wird die Generatorfunktion foo jedoch das nächste Mal, wenn der Generator nach einem Wert gefragt wird, dort fortgesetzt, wo sie aufgehört hat - nach der letzten Yield-Anweisung - und läuft weiter, bis sie eine andere Yield-Anweisung trifft.Wenn Sie hinter den Kulissen
bar=foo(6)
die Generatorobjektleiste aufrufen, wird definiert, dass Sie einnext
Attribut haben.Sie können es selbst aufrufen, um Werte aus foo abzurufen:
Wenn foo endet (und es keine Werte mehr gibt),
next(bar)
löst der Aufruf einen StopInteration-Fehler aus.quelle
In diesem Beitrag werden Fibonacci-Zahlen als Hilfsmittel verwendet, um die Nützlichkeit von Python-Generatoren zu erläutern .
Dieser Beitrag enthält sowohl C ++ - als auch Python-Code.
Fibonacci-Zahlen sind wie folgt definiert: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....
Oder allgemein:
Dies kann sehr einfach in eine C ++ - Funktion übertragen werden:
Wenn Sie jedoch die ersten sechs Fibonacci-Zahlen drucken möchten, berechnen Sie viele der Werte mit der obigen Funktion neu.
Zum Beispiel:
Fib(3) = Fib(2) + Fib(1)
aberFib(2)
auch neu berechnetFib(1)
. Je höher der Wert, den Sie berechnen möchten, desto schlechter wird es Ihnen gehen.Man könnte also versucht sein, das Obige neu zu schreiben, indem man den Zustand im Auge behält
main
.Aber das ist sehr hässlich und kompliziert unsere Logik
main
. Es wäre besser, sich in unserermain
Funktion keine Sorgen um den Zustand machen zu müssen .Wir könnten einen
vector
von Werten zurückgeben und einen verwendeniterator
, um über diesen Satz von Werten zu iterieren, aber dies erfordert viel Speicher auf einmal für eine große Anzahl von Rückgabewerten.Zurück zu unserem alten Ansatz: Was passiert, wenn wir neben dem Drucken der Zahlen noch etwas anderes tun möchten? Wir müssten den gesamten Codeblock kopieren und einfügen
main
und die Ausgabeanweisungen in das ändern, was wir sonst noch tun wollten. Und wenn Sie Code kopieren und einfügen, sollten Sie erschossen werden. Du willst doch nicht erschossen werden, oder?Um diese Probleme zu lösen und um nicht erschossen zu werden, können wir diesen Codeblock mithilfe einer Rückruffunktion neu schreiben. Jedes Mal, wenn eine neue Fibonacci-Nummer gefunden wird, rufen wir die Rückruffunktion auf.
Dies ist eindeutig eine Verbesserung, Ihre Logik
main
ist nicht so überladen, und Sie können mit den Fibonacci-Nummern alles tun, was Sie wollen. Definieren Sie einfach neue Rückrufe.Das ist aber immer noch nicht perfekt. Was wäre, wenn Sie nur die ersten beiden Fibonacci-Zahlen erhalten und dann etwas tun möchten, dann etwas mehr und dann etwas anderes?
Nun, wir könnten so weitermachen wie bisher und den Status erneut hinzufügen
main
, sodass GetFibNumbers von einem beliebigen Punkt aus starten kann. Dies wird unseren Code jedoch weiter aufblähen und sieht für eine einfache Aufgabe wie das Drucken von Fibonacci-Zahlen bereits zu groß aus.Wir könnten ein Produzenten- und Konsumentenmodell über ein paar Threads implementieren. Dies macht den Code jedoch noch komplizierter.
Sprechen wir stattdessen über Generatoren.
Python hat eine sehr schöne Sprachfunktion, die Probleme wie diese sogenannten Generatoren löst.
Mit einem Generator können Sie eine Funktion ausführen, an einem beliebigen Punkt anhalten und dann dort fortfahren, wo Sie aufgehört haben. Jedes Mal, wenn ein Wert zurückgegeben wird.
Betrachten Sie den folgenden Code, der einen Generator verwendet:
Welches gibt uns die Ergebnisse:
Die
yield
Anweisung wird in Verbindung mit Python-Generatoren verwendet. Es speichert den Status der Funktion und gibt den erstellten Wert zurück. Wenn Sie das nächste Mal die Funktion next () am Generator aufrufen, wird sie dort fortgesetzt, wo die Ausbeute aufgehört hat.Dies ist weitaus sauberer als der Rückruffunktionscode. Wir haben saubereren Code, kleineren Code und viel mehr funktionalen Code (Python erlaubt beliebig große ganze Zahlen).
Quelle
quelle
Ich glaube, das erste Auftreten von Iteratoren und Generatoren erfolgte vor etwa 20 Jahren in der Programmiersprache Icon.
Sie können die Symbolübersicht genießen , mit der Sie Ihren Kopf um sie wickeln können, ohne sich auf die Syntax zu konzentrieren (da Symbol eine Sprache ist, die Sie wahrscheinlich nicht kennen, und Griswold Menschen aus anderen Sprachen die Vorteile seiner Sprache erklärte).
Nachdem Sie dort nur einige Absätze gelesen haben, wird der Nutzen von Generatoren und Iteratoren möglicherweise deutlicher.
quelle
Die Erfahrung mit Listenverständnissen hat gezeigt, dass sie in Python weit verbreitet sind. In vielen Anwendungsfällen muss jedoch keine vollständige Liste im Speicher erstellt werden. Stattdessen müssen sie die Elemente nur einzeln durchlaufen.
Mit dem folgenden Summierungscode wird beispielsweise eine vollständige Liste der Quadrate im Speicher erstellt, diese Werte durchlaufen und die Liste gelöscht, wenn die Referenz nicht mehr benötigt wird:
sum([x*x for x in range(10)])
Der Speicher wird durch Verwendung eines Generatorausdrucks erhalten:
sum(x*x for x in range(10))
Konstruktoren für Containerobjekte erhalten ähnliche Vorteile:
Generatorausdrücke sind besonders nützlich bei Funktionen wie sum (), min () und max (), die eine iterierbare Eingabe auf einen einzelnen Wert reduzieren:
Mehr
quelle
Ich habe diesen Code erstellt, der drei Schlüsselkonzepte zu Generatoren erklärt:
quelle