Generatoren in Python verstehen

218

Ich lese gerade das Python-Kochbuch und suche derzeit nach Generatoren. Es fällt mir schwer, meinen Kopf herumzukriegen.

Gibt es ein Java-Äquivalent, da ich einen Java-Hintergrund habe? Das Buch sprach über "Produzent / Konsument", aber wenn ich höre, dass ich an Threading denke.

Was ist ein Generator und warum würden Sie ihn verwenden? Natürlich ohne Bücher zu zitieren (es sei denn, Sie finden eine anständige, vereinfachende Antwort direkt aus einem Buch). Vielleicht mit Beispielen, wenn Sie sich großzügig fühlen!

Federer
quelle

Antworten:

402

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 StopIterationAusnahme 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 yieldbeliebigen 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 nund n + 1. Jeder Aufruf von nextergibt einen einzelnen Wert, bis alle Werte erhalten wurden. forSchleifen rufen nextim 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, nextum den Code tatsächlich (teilweise) auszuführen. Die Ausführung des Codes in einem Generator stoppt, sobald eine yieldAnweisung erreicht wurde, bei der ein Wert zurückgegeben wird. Der nächste Aufruf von nextbewirkt 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, sendDaten 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 itertoolsModul genau anzusehen , da sie wichtige Werkzeuge für das einfache Schreiben fortschrittlicher Generatoren sind.


   Über Python <= 2.6: In den obigen Beispielen nextist 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, .nextsodass Sie in 2.7 nicht Folgendes verwenden müssen:

>>> g = (n for n in range(3, 5))
>>> g.next()
3
Stephan202
quelle
9
Sie erwähnen, dass es möglich ist, sendDaten 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 kein Locks 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.
Jochen Ritzel
Sind Python-Generatoren im Grunde Turing-Maschinen in Bezug auf ihre Funktionsweise?
Feuriger Phönix
48

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.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

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:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Beachten Sie, dass Generatoren beispielsweise eine andere Möglichkeit bieten, mit Unendlichkeit umzugehen

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

Der Generator kapselt eine Endlosschleife, aber dies ist kein Problem, da Sie jede Antwort nur jedes Mal erhalten, wenn Sie danach fragen.

Caleb Hattingh
quelle
30

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.

Nikow
quelle
2
Hmm, ich denke du hast recht, zumindest nach einem Test einiger Zeilen in Python 2.6. Ein Generatorausdruck gibt einen Iterator (auch als "Generatorobjekt" bezeichnet) zurück, keinen Generator.
Craig McQueen
22

Generatoren können als Abkürzung für die Erstellung eines Iterators angesehen werden. Sie verhalten sich wie ein Java-Iterator. Beispiel:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

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:

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...
überdenken
quelle
1
Jetzt hat Java Streams, die Generatoren viel ähnlicher sind, außer dass Sie anscheinend nicht einfach das nächste Element ohne überraschend viel Aufwand erhalten können.
Fund Monica Klage
12

Es gibt kein Java-Äquivalent.

Hier ist ein erfundenes Beispiel:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

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 forSchleife 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.

Wernsey
quelle
2
Der letzte Absatz ist sehr wichtig: Der Status der Generatorfunktion wird jedes Mal "eingefroren", wenn sie etw liefert, und wird beim nächsten Aufruf in genau demselben Status fortgesetzt.
Johannes Charra
In Java gibt es kein syntaktisches Äquivalent zu einem "Generatorausdruck", aber Generatoren - sobald Sie einen haben - sind im Wesentlichen nur ein Iterator (dieselben grundlegenden Eigenschaften wie ein Java-Iterator).
Überdenken Sie den
@overthink: Nun, Generatoren können andere Nebenwirkungen haben, die Java-Iteratoren nicht haben können. Wenn ich setzen war , print "hello"nachdem die x=x+1in 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.
Wernsey
@iWerner: Ziemlich sicher, dass der gleiche Effekt in Java erzielt werden kann. Die Implementierung von next () im entsprechenden Java-Iterator müsste immer noch von 0 bis 99 suchen (anhand Ihres Beispiels mygen (100)), sodass Sie jedes Mal System.out.println () verwenden können, wenn Sie möchten. Sie würden jedoch nur 33 Mal von next () zurückkehren. Was Java fehlt, ist die sehr praktische Ertragssyntax, die wesentlich einfacher zu lesen (und zu schreiben) ist.
Überdenken Sie den
Ich habe es geliebt, diese einzeilige Definition zu lesen und mich daran zu erinnern: Wenn der Generator zum ersten Mal ausgeführt wird, beginnt er am Anfang, andernfalls setzt er sich gegenüber der vorherigen Ausgabe fort.
Iqra.
8

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.

Peter Hansen
quelle
6

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/ .

Robert Rossney
quelle
6

Es hilft, klar zwischen der Funktion foo und dem Generator foo (n) zu unterscheiden:

def foo(n):
    yield n
    yield n+1

foo ist eine Funktion. foo (6) ist ein Generatorobjekt.

Die typische Art, ein Generatorobjekt zu verwenden, ist eine Schleife:

for n in foo(6):
    print(n)

Die Schleife wird gedruckt

# 6
# 7

Stellen Sie sich einen Generator als eine wiederaufnehmbare Funktion vor.

yieldverhält sich wie returnin 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 ein nextAttribut haben.

Sie können es selbst aufrufen, um Werte aus foo abzurufen:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Wenn foo endet (und es keine Werte mehr gibt), next(bar)löst der Aufruf einen StopInteration-Fehler aus.

unutbu
quelle
5

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:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Dies kann sehr einfach in eine C ++ - Funktion übertragen werden:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

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)aber Fib(2)auch neu berechnet Fib(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.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Aber das ist sehr hässlich und kompliziert unsere Logik main. Es wäre besser, sich in unserer mainFunktion keine Sorgen um den Zustand machen zu müssen .

Wir könnten einen vectorvon Werten zurückgeben und einen verwenden iterator, 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 mainund 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.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Dies ist eindeutig eine Verbesserung, Ihre Logik mainist 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:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Welches gibt uns die Ergebnisse:

0 1 1 2 3 5

Die yieldAnweisung 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

Brian R. Bondy
quelle
3

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.

Nosredna
quelle
2

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:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

Generatorausdrücke sind besonders nützlich bei Funktionen wie sum (), min () und max (), die eine iterierbare Eingabe auf einen einzelnen Wert reduzieren:

max(len(line)  for line in file  if line.strip())

Mehr

Saqib Mujtaba
quelle
1

Ich habe diesen Code erstellt, der drei Schlüsselkonzepte zu Generatoren erklärt:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
Stefan Iancu
quelle