Wofür können Sie Python-Generatorfunktionen verwenden?
213
Ich fange an, Python zu lernen, und bin auf Generatorfunktionen gestoßen, die eine Yield-Anweisung enthalten. Ich möchte wissen, welche Arten von Problemen diese Funktionen wirklich gut lösen können.
Generatoren geben Ihnen eine träge Bewertung. Sie verwenden sie, indem Sie über sie iterieren, entweder explizit mit 'for' oder implizit, indem Sie sie an eine Funktion oder ein Konstrukt übergeben, das iteriert. Sie können sich Generatoren so vorstellen, als würden sie mehrere Elemente zurückgeben, als würden sie eine Liste zurückgeben. Statt jedoch alle auf einmal zurückzugeben, geben sie diese einzeln zurück, und die Generatorfunktion wird angehalten, bis das nächste Element angefordert wird.
Generatoren eignen sich gut zum Berechnen großer Ergebnissätze (insbesondere Berechnungen mit Schleifen selbst), bei denen Sie nicht wissen, ob Sie alle Ergebnisse benötigen oder bei denen Sie nicht den Speicher für alle Ergebnisse gleichzeitig zuweisen möchten . Oder für Situationen, in denen der Generator einen anderen Generator verwendet oder eine andere Ressource verbraucht, und es bequemer ist, wenn dies so spät wie möglich geschieht.
Eine andere Verwendung für Generatoren (die wirklich dieselbe ist) besteht darin, Rückrufe durch Iteration zu ersetzen. In einigen Situationen möchten Sie, dass eine Funktion viel Arbeit leistet und gelegentlich dem Anrufer Bericht erstattet. Traditionell verwenden Sie hierfür eine Rückruffunktion. Sie übergeben diesen Rückruf an die Work-Funktion, die diesen Rückruf regelmäßig aufruft. Der Generatoransatz besteht darin, dass die Work-Funktion (jetzt ein Generator) nichts über den Rückruf weiß und nur dann nachgibt, wenn sie etwas melden möchte. Anstatt einen separaten Rückruf zu schreiben und diesen an die Work-Funktion zu übergeben, erledigt der Aufrufer die gesamte Berichterstellung in einer kleinen 'for'-Schleife um den Generator.
Angenommen, Sie haben ein Programm zur Suche nach Dateisystemen geschrieben. Sie können die Suche vollständig durchführen, die Ergebnisse sammeln und dann einzeln anzeigen. Alle Ergebnisse müssten gesammelt werden, bevor Sie das erste zeigen, und alle Ergebnisse würden gleichzeitig gespeichert. Oder Sie können die Ergebnisse anzeigen, während Sie sie finden. Dies wäre speichereffizienter und für den Benutzer viel freundlicher. Letzteres kann erreicht werden, indem die Ergebnisdruckfunktion an die Dateisystem-Suchfunktion übergeben wird, oder indem einfach die Suchfunktion zu einem Generator gemacht und das Ergebnis durchlaufen wird.
Wenn Sie ein Beispiel für die beiden letztgenannten Ansätze sehen möchten, lesen Sie os.path.walk () (die alte Dateisystem-Walking-Funktion mit Rückruf) und os.walk () (den neuen Dateisystem-Walking-Generator) Sie wollten wirklich alle Ergebnisse in einer Liste sammeln. Der Generator-Ansatz ist trivial, um ihn in den Big-List-Ansatz umzuwandeln:
Führt ein Generator wie einer, der Dateisystemlisten erstellt, Aktionen parallel zu dem Code aus, der diesen Generator in einer Schleife ausführt? Im Idealfall würde der Computer den Hauptteil der Schleife ausführen (das letzte Ergebnis verarbeiten) und gleichzeitig alles tun, was der Generator tun muss, um den nächsten Wert zu erhalten.
Steven Lu
@StevenLu: Sofern es nicht schwierig ist, Threads manuell vor yieldund joinnach dem nächsten Ergebnis zu starten , wird es nicht parallel ausgeführt (und kein Standardbibliotheksgenerator tut dies; das heimliche Starten von Threads ist verpönt). Der Generator hält jeweils an, yieldbis der nächste Wert angefordert wird. Wenn der Generator E / A umschließt, speichert das Betriebssystem möglicherweise proaktiv Daten aus der Datei zwischen, sofern davon ausgegangen wird, dass sie in Kürze angefordert werden. Dies ist jedoch das Betriebssystem, an dem Python nicht beteiligt ist.
ShadowRanger
90
Einer der Gründe für die Verwendung eines Generators besteht darin, die Lösung für bestimmte Lösungen klarer zu gestalten.
Die andere Möglichkeit besteht darin, die Ergebnisse einzeln zu behandeln und zu vermeiden, dass riesige Listen von Ergebnissen erstellt werden, die Sie ohnehin getrennt verarbeiten würden.
Wenn Sie eine Fibonacci-up-to-n-Funktion wie diese haben:
# function versiondef fibon(n):
a = b =1
result =[]for i in xrange(n):
result.append(a)
a, b = b, a + b
return result
Sie können die Funktion einfacher wie folgt schreiben:
# generator versiondef fibon(n):
a = b =1for i in xrange(n):yield a
a, b = b, a + b
Die Funktion ist klarer. Und wenn Sie die Funktion so nutzen:
for x in fibon(1000000):print x,
In diesem Beispiel wird bei Verwendung der Generatorversion nicht die gesamte 1000000-Artikelliste erstellt, sondern jeweils nur ein Wert. Dies wäre bei Verwendung der Listenversion nicht der Fall, bei der zuerst eine Liste erstellt wird.
Eine nicht offensichtliche Verwendung von Generatoren ist das Erstellen unterbrechbarer Funktionen, mit denen Sie beispielsweise die Benutzeroberfläche aktualisieren oder mehrere Jobs "gleichzeitig" (eigentlich verschachtelt) ausführen können, ohne Threads zu verwenden.
Der Abschnitt Motivation ist insofern schön, als er ein spezifisches Beispiel enthält: "Wenn eine Producer-Funktion so hart ist, dass der Zustand zwischen den produzierten Werten beibehalten werden muss, bieten die meisten Programmiersprachen keine angenehme und effiziente Lösung, außer dem Argument des Produzenten eine Rückruffunktion hinzuzufügen list ... Zum Beispiel verfolgt tokenize.py in der Standardbibliothek diesen Ansatz "
Ben Creasy
38
Ich finde diese Erklärung, die meine Zweifel beseitigt. Weil es eine Möglichkeit gibt, dass eine Person, die es nicht weiß, Generatorsauch nichts davon weißyield
Rückkehr
In der return-Anweisung werden alle lokalen Variablen zerstört und der resultierende Wert wird an den Aufrufer zurückgegeben (zurückgegeben). Sollte dieselbe Funktion einige Zeit später aufgerufen werden, erhält die Funktion einen neuen Satz von Variablen.
Ausbeute
Was aber, wenn die lokalen Variablen beim Beenden einer Funktion nicht weggeworfen werden? Dies bedeutet, dass wir dort können, resume the functionwo wir aufgehört haben. Hier wird das Konzept von generatorseingeführt und die yieldAnweisung dort fortgesetzt, wo das functionaufgehört hat.
def generate_integers(N):for i in xrange(N):yield i
In[1]: gen = generate_integers(3)In[2]: gen
<generator object at 0x8117f90>In[3]: gen.next()0In[4]: gen.next()1In[5]: gen.next()
Das ist also der Unterschied zwischen returnund yieldAnweisungen in Python.
Die Yield-Anweisung macht eine Funktion zu einer Generatorfunktion.
Generatoren sind daher ein einfaches und leistungsstarkes Werkzeug zum Erstellen von Iteratoren. Sie sind wie reguläre Funktionen geschrieben, verwenden die yieldAnweisung jedoch immer dann, wenn sie Daten zurückgeben möchten. Bei jedem Aufruf von next () wird der Generator dort fortgesetzt, wo er aufgehört hat (er merkt sich alle Datenwerte und welche Anweisung zuletzt ausgeführt wurde).
Angenommen, Sie haben 100 Millionen Domains in Ihrer MySQL-Tabelle und möchten den Alexa-Rang für jede Domain aktualisieren.
Als erstes müssen Sie Ihre Domain-Namen aus der Datenbank auswählen.
Angenommen, Ihr Tabellenname lautet domainsund der Spaltenname lautet domain.
Wenn Sie verwenden SELECT domain FROM domains, werden 100 Millionen Zeilen zurückgegeben, was viel Speicherplatz beansprucht. Ihr Server könnte also abstürzen.
Sie haben sich also entschlossen, das Programm stapelweise auszuführen. Angenommen, unsere Chargengröße beträgt 1000.
In unserem ersten Stapel werden wir die ersten 1000 Zeilen abfragen, den Alexa-Rang für jede Domain überprüfen und die Datenbankzeile aktualisieren.
In unserer zweiten Charge werden wir an den nächsten 1000 Zeilen arbeiten. In unserer dritten Charge wird es von 2001 bis 3000 sein und so weiter.
Jetzt brauchen wir eine Generatorfunktion, die unsere Chargen generiert.
Hier ist unsere Generatorfunktion:
defResultGenerator(cursor, batchsize=1000):whileTrue:
results = cursor.fetchmany(batchsize)ifnot results:breakfor result in results:yield result
Wie Sie sehen können, behält unsere Funktion yielddie Ergebnisse bei. Wenn Sie das Schlüsselwort returnanstelle von verwenden yield, wird die gesamte Funktion beendet, sobald sie return erreicht.
return- returns only once
yield- returns multiple times
Wenn eine Funktion das Schlüsselwort verwendet, handelt yieldes sich um einen Generator.
Jetzt können Sie folgendermaßen iterieren:
db =MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")for result inResultGenerator(cursor):
doSomethingWith(result)
db.close()
Es wäre praktischer, wenn die Ausbeute durch rekursive / dyanmische Programmierung erklärt werden könnte!
Igaurav
27
Pufferung. Wenn es effizient ist, Daten in großen Blöcken abzurufen, aber in kleinen Blöcken zu verarbeiten, kann ein Generator helfen:
def bufferedFetch():whileTrue:
buffer = getBigChunkOfData()# insert some code to break on 'end of data'for i in buffer:yield i
Mit den obigen Anweisungen können Sie die Pufferung einfach von der Verarbeitung trennen. Die Consumer-Funktion kann jetzt nur die Werte einzeln abrufen, ohne sich um die Pufferung kümmern zu müssen.
Wenn getBigChuckOfData nicht faul ist, verstehe ich nicht, welchen Nutzen die Rendite hier hat. Was ist ein Anwendungsfall für diese Funktion?
Sean Geoffrey Pietz
1
Aber der Punkt ist , dass IIUC, bufferedFetch wird lazyfying den Anruf zu getBigChunkOfData. Wenn getBigChunkOfData bereits faul wäre, wäre bufferedFetch nutzlos. Jeder Aufruf von bufferedFetch () gibt ein Pufferelement zurück, obwohl bereits ein BigChunk eingelesen wurde. Und Sie müssen das nächste zurückzugebende Element nicht explizit zählen, da die Mechanik der Ausbeute genau dies implizit tut.
Hmijail trauert um Rücktritte
21
Ich habe festgestellt, dass Generatoren sehr hilfreich sind, um Ihren Code zu bereinigen und Ihnen eine einzigartige Möglichkeit zu bieten, Code zu kapseln und zu modularisieren. In einer Situation, in der Sie etwas benötigen, um Werte basierend auf der eigenen internen Verarbeitung ständig auszuspucken, und wenn dieses Element von einer beliebigen Stelle in Ihrem Code (und nicht nur innerhalb einer Schleife oder eines Blocks) aufgerufen werden muss, sind Generatoren die Funktion dafür verwenden.
Ein abstraktes Beispiel wäre ein Fibonacci-Zahlengenerator, der nicht in einer Schleife lebt und bei einem Aufruf von überall immer die nächste Zahl in der Sequenz zurückgibt:
def fib():
first =0
second =1yield first
yield second
while1:
next = first + second
yield next
first = second
second = next
fibgen1 = fib()
fibgen2 = fib()
Jetzt haben Sie zwei Fibonacci-Nummerngeneratorobjekte, die Sie von überall in Ihrem Code aufrufen können, und sie geben immer größere Fibonacci-Nummern nacheinander wie folgt zurück:
Das Schöne an Generatoren ist, dass sie den Zustand einkapseln, ohne die Rahmen für die Erstellung von Objekten durchlaufen zu müssen. Eine Art, über sie nachzudenken, sind "Funktionen", die sich an ihren inneren Zustand erinnern.
Ich habe das Fibonacci-Beispiel von Python Generators erhalten - Was sind sie? Mit ein wenig Fantasie können Sie sich viele andere Situationen einfallen lassen, in denen Generatoren eine großartige Alternative zu forSchleifen und anderen traditionellen Iterationskonstrukten darstellen.
Die einfache Erklärung: Betrachten Sie eine forAussage
for item in iterable:
do_stuff()
In den meisten iterableFällen müssen nicht alle Elemente von Anfang an vorhanden sein, sondern können bei Bedarf im laufenden Betrieb generiert werden. Dies kann in beiden Fällen viel effizienter sein
Speicherplatz (Sie müssen nie alle Elemente gleichzeitig speichern) und
Zeit (die Iteration kann beendet sein, bevor alle Elemente benötigt werden).
In anderen Fällen kennen Sie nicht einmal alle Elemente im Voraus. Beispielsweise:
for command in user_input():
do_stuff_with(command)
Sie haben keine Möglichkeit, alle Befehle des Benutzers im Voraus zu kennen, aber Sie können eine schöne Schleife wie diese verwenden, wenn ein Generator Ihnen Befehle übergibt:
... und eine unendliche Sequenz könnte eine sein, die durch wiederholtes Durchlaufen einer kleinen Liste erzeugt wird und nach Erreichen des Endes zum Anfang zurückkehrt. Ich verwende dies, um Farben in Diagrammen auszuwählen oder um beschäftigte Pocher oder Spinner im Text zu erzeugen.
Andrej Panjkov
@mataap: Dafür gibt es eine itertool- siehe cycles.
Martineau
12
Meine bevorzugten Anwendungen sind "Filtern" und "Reduzieren".
Angenommen, wir lesen eine Datei und möchten nur die Zeilen, die mit "##" beginnen.
def filter2sharps( aSequence ):for l in aSequence:if l.startswith("##"):yield l
Wir können dann die Generatorfunktion in einer richtigen Schleife verwenden
source= file(...)for line in filter2sharps( source.readlines()):print line
source.close()
Das Reduzierungsbeispiel ist ähnlich. Angenommen, wir haben eine Datei, in der wir Zeilenblöcke suchen müssen <Location>...</Location>. [Keine HTML-Tags, sondern Zeilen, die tagartig aussehen.]
def reduceLocation( aSequence ):
keep=False
block=Nonefor line in aSequence:if line.startswith("</Location"):
block.append( line )yield block
block=None
keep=Falseelif line.startsWith("<Location"):
block=[ line ]
keep=Trueelif keep:
block.append( line )else:passif block isnotNone:yield block # A partial block, icky
Auch hier können wir diesen Generator in einer geeigneten for-Schleife verwenden.
source = file(...)for b in reduceLocation( source.readlines()):print b
source.close()
Die Idee ist, dass eine Generatorfunktion es uns ermöglicht, eine Sequenz zu filtern oder zu reduzieren, indem jeweils eine andere Sequenz mit einem Wert erzeugt wird.
fileobj.readlines()würde die gesamte Datei in eine Liste im Speicher lesen und den Zweck der Verwendung von Generatoren zunichte machen. Da Dateiobjekte bereits iterierbar sind, können Sie sie for b in your_generator(fileobject):stattdessen verwenden. Auf diese Weise wird Ihre Datei zeilenweise gelesen, um das Lesen der gesamten Datei zu vermeiden.
Nosklo
reductLocation ist ziemlich seltsam, wenn man eine Liste liefert. Warum nicht einfach jede Zeile? Auch Filtern und Reduzieren sind integrierte Funktionen mit erwartetem Verhalten (siehe Hilfe in Ipython usw.). Ihre Verwendung von "Reduzieren" entspricht der von Filter.
James Antill
Guter Punkt in den Readlines (). Normalerweise stelle ich fest, dass Dateien beim Unit-Test erstklassige Zeileniteratoren sind.
S.Lott
Tatsächlich kombiniert die "Reduktion" mehrere einzelne Linien zu einem zusammengesetzten Objekt. Okay, es ist eine Liste, aber es ist immer noch eine Reduktion aus der Quelle.
S.Lott
9
Ein praktisches Beispiel, bei dem Sie einen Generator verwenden könnten, ist, wenn Sie eine Form haben und über seine Ecken, Kanten oder was auch immer iterieren möchten. Für mein eigenes Projekt (Quellcode hier ) hatte ich ein Rechteck:
Jetzt kann ich ein Rechteck erstellen und seine Ecken durchlaufen:
myrect=Rect(50,50,100,100)for corner in myrect:print(corner)
Stattdessen __iter__könnten Sie eine Methode haben iter_cornersund diese mit aufrufen for corner in myrect.iter_corners(). Es ist nur eleganter zu verwenden, __iter__da wir den Namen der Klasseninstanz direkt im forAusdruck verwenden können.
Einige gute Antworten hier, ich würde jedoch auch empfehlen, das Tutorial zur funktionalen Programmierung von Python vollständig zu lesen, um einige der leistungsstärkeren Anwendungsfälle von Generatoren zu erklären.
Da die Sendemethode eines Generators nicht erwähnt wurde, ist hier ein Beispiel:
def test():for i in xrange(5):
val =yieldprint(val)
t = test()# Proceed to 'yield' statement
next(t)# Send value to yield
t.send(1)
t.send('2')
t.send([3])
Es zeigt die Möglichkeit, einen Wert an einen laufenden Generator zu senden. Ein weiterführender Kurs zu Generatoren im folgenden Video (einschließlich yieldExploration, Generatoren für die Parallelverarbeitung, Überschreiten der Rekursionsgrenze usw.)
Haufenweise Sachen. Jedes Mal, wenn Sie eine Folge von Elementen generieren möchten, diese aber nicht alle gleichzeitig in einer Liste "materialisieren" müssen. Sie könnten beispielsweise einen einfachen Generator haben, der Primzahlen zurückgibt:
def primes():
primes_found = set()
primes_found.add(2)yield2for i in itertools.count(1):
candidate = i *2+1ifnot all(candidate % prime for prime in primes_found):
primes_found.add(candidate)yield candidate
Sie können dies dann verwenden, um die Produkte nachfolgender Primzahlen zu generieren:
def prime_products():
primeiter = primes()
prev = primeiter.next()for prime in primeiter:yield prime * prev
prev = prime
Dies sind ziemlich triviale Beispiele, aber Sie können sehen, wie nützlich es sein kann, große (möglicherweise unendliche!) Datensätze zu verarbeiten, ohne sie vorher zu generieren, was nur eine der offensichtlicheren Anwendungen ist.
wenn nicht einer (Kandidat% prime für prime in prim_found) sollte sein, wenn alle (Kandidat% prime für prime in prim_found)
rjmunro
Ja, ich wollte schreiben "wenn nicht welche (Kandidat% prime == 0 für prime in primes_found). Ihre ist jedoch etwas ordentlicher. :)
Nick Johnson
Ich denke, Sie haben vergessen, das 'nicht' von wenn nicht allen zu löschen (Kandidat% prime für prime in primes_found)
Thava
0
Auch zum Drucken der Primzahlen bis n geeignet:
def genprime(n=10):for num in range(3, n+1):for factor in range(2, num):if num%factor ==0:breakelse:yield(num)for prime_num in genprime(100):print(prime_num)
Antworten:
Generatoren geben Ihnen eine träge Bewertung. Sie verwenden sie, indem Sie über sie iterieren, entweder explizit mit 'for' oder implizit, indem Sie sie an eine Funktion oder ein Konstrukt übergeben, das iteriert. Sie können sich Generatoren so vorstellen, als würden sie mehrere Elemente zurückgeben, als würden sie eine Liste zurückgeben. Statt jedoch alle auf einmal zurückzugeben, geben sie diese einzeln zurück, und die Generatorfunktion wird angehalten, bis das nächste Element angefordert wird.
Generatoren eignen sich gut zum Berechnen großer Ergebnissätze (insbesondere Berechnungen mit Schleifen selbst), bei denen Sie nicht wissen, ob Sie alle Ergebnisse benötigen oder bei denen Sie nicht den Speicher für alle Ergebnisse gleichzeitig zuweisen möchten . Oder für Situationen, in denen der Generator einen anderen Generator verwendet oder eine andere Ressource verbraucht, und es bequemer ist, wenn dies so spät wie möglich geschieht.
Eine andere Verwendung für Generatoren (die wirklich dieselbe ist) besteht darin, Rückrufe durch Iteration zu ersetzen. In einigen Situationen möchten Sie, dass eine Funktion viel Arbeit leistet und gelegentlich dem Anrufer Bericht erstattet. Traditionell verwenden Sie hierfür eine Rückruffunktion. Sie übergeben diesen Rückruf an die Work-Funktion, die diesen Rückruf regelmäßig aufruft. Der Generatoransatz besteht darin, dass die Work-Funktion (jetzt ein Generator) nichts über den Rückruf weiß und nur dann nachgibt, wenn sie etwas melden möchte. Anstatt einen separaten Rückruf zu schreiben und diesen an die Work-Funktion zu übergeben, erledigt der Aufrufer die gesamte Berichterstellung in einer kleinen 'for'-Schleife um den Generator.
Angenommen, Sie haben ein Programm zur Suche nach Dateisystemen geschrieben. Sie können die Suche vollständig durchführen, die Ergebnisse sammeln und dann einzeln anzeigen. Alle Ergebnisse müssten gesammelt werden, bevor Sie das erste zeigen, und alle Ergebnisse würden gleichzeitig gespeichert. Oder Sie können die Ergebnisse anzeigen, während Sie sie finden. Dies wäre speichereffizienter und für den Benutzer viel freundlicher. Letzteres kann erreicht werden, indem die Ergebnisdruckfunktion an die Dateisystem-Suchfunktion übergeben wird, oder indem einfach die Suchfunktion zu einem Generator gemacht und das Ergebnis durchlaufen wird.
Wenn Sie ein Beispiel für die beiden letztgenannten Ansätze sehen möchten, lesen Sie os.path.walk () (die alte Dateisystem-Walking-Funktion mit Rückruf) und os.walk () (den neuen Dateisystem-Walking-Generator) Sie wollten wirklich alle Ergebnisse in einer Liste sammeln. Der Generator-Ansatz ist trivial, um ihn in den Big-List-Ansatz umzuwandeln:
quelle
yield
undjoin
nach dem nächsten Ergebnis zu starten , wird es nicht parallel ausgeführt (und kein Standardbibliotheksgenerator tut dies; das heimliche Starten von Threads ist verpönt). Der Generator hält jeweils an,yield
bis der nächste Wert angefordert wird. Wenn der Generator E / A umschließt, speichert das Betriebssystem möglicherweise proaktiv Daten aus der Datei zwischen, sofern davon ausgegangen wird, dass sie in Kürze angefordert werden. Dies ist jedoch das Betriebssystem, an dem Python nicht beteiligt ist.Einer der Gründe für die Verwendung eines Generators besteht darin, die Lösung für bestimmte Lösungen klarer zu gestalten.
Die andere Möglichkeit besteht darin, die Ergebnisse einzeln zu behandeln und zu vermeiden, dass riesige Listen von Ergebnissen erstellt werden, die Sie ohnehin getrennt verarbeiten würden.
Wenn Sie eine Fibonacci-up-to-n-Funktion wie diese haben:
Sie können die Funktion einfacher wie folgt schreiben:
Die Funktion ist klarer. Und wenn Sie die Funktion so nutzen:
In diesem Beispiel wird bei Verwendung der Generatorversion nicht die gesamte 1000000-Artikelliste erstellt, sondern jeweils nur ein Wert. Dies wäre bei Verwendung der Listenversion nicht der Fall, bei der zuerst eine Liste erstellt wird.
quelle
list(fibon(5))
Siehe den Abschnitt "Motivation" in PEP 255 .
Eine nicht offensichtliche Verwendung von Generatoren ist das Erstellen unterbrechbarer Funktionen, mit denen Sie beispielsweise die Benutzeroberfläche aktualisieren oder mehrere Jobs "gleichzeitig" (eigentlich verschachtelt) ausführen können, ohne Threads zu verwenden.
quelle
Ich finde diese Erklärung, die meine Zweifel beseitigt. Weil es eine Möglichkeit gibt, dass eine Person, die es nicht weiß,
Generators
auch nichts davon weißyield
Rückkehr
In der return-Anweisung werden alle lokalen Variablen zerstört und der resultierende Wert wird an den Aufrufer zurückgegeben (zurückgegeben). Sollte dieselbe Funktion einige Zeit später aufgerufen werden, erhält die Funktion einen neuen Satz von Variablen.
Ausbeute
Was aber, wenn die lokalen Variablen beim Beenden einer Funktion nicht weggeworfen werden? Dies bedeutet, dass wir dort können,
resume the function
wo wir aufgehört haben. Hier wird das Konzept vongenerators
eingeführt und dieyield
Anweisung dort fortgesetzt, wo dasfunction
aufgehört hat.Das ist also der Unterschied zwischen
return
undyield
Anweisungen in Python.Die Yield-Anweisung macht eine Funktion zu einer Generatorfunktion.
Generatoren sind daher ein einfaches und leistungsstarkes Werkzeug zum Erstellen von Iteratoren. Sie sind wie reguläre Funktionen geschrieben, verwenden die
yield
Anweisung jedoch immer dann, wenn sie Daten zurückgeben möchten. Bei jedem Aufruf von next () wird der Generator dort fortgesetzt, wo er aufgehört hat (er merkt sich alle Datenwerte und welche Anweisung zuletzt ausgeführt wurde).quelle
Beispiel aus der realen Welt
Angenommen, Sie haben 100 Millionen Domains in Ihrer MySQL-Tabelle und möchten den Alexa-Rang für jede Domain aktualisieren.
Als erstes müssen Sie Ihre Domain-Namen aus der Datenbank auswählen.
Angenommen, Ihr Tabellenname lautet
domains
und der Spaltenname lautetdomain
.Wenn Sie verwenden
SELECT domain FROM domains
, werden 100 Millionen Zeilen zurückgegeben, was viel Speicherplatz beansprucht. Ihr Server könnte also abstürzen.Sie haben sich also entschlossen, das Programm stapelweise auszuführen. Angenommen, unsere Chargengröße beträgt 1000.
In unserem ersten Stapel werden wir die ersten 1000 Zeilen abfragen, den Alexa-Rang für jede Domain überprüfen und die Datenbankzeile aktualisieren.
In unserer zweiten Charge werden wir an den nächsten 1000 Zeilen arbeiten. In unserer dritten Charge wird es von 2001 bis 3000 sein und so weiter.
Jetzt brauchen wir eine Generatorfunktion, die unsere Chargen generiert.
Hier ist unsere Generatorfunktion:
Wie Sie sehen können, behält unsere Funktion
yield
die Ergebnisse bei. Wenn Sie das Schlüsselwortreturn
anstelle von verwendenyield
, wird die gesamte Funktion beendet, sobald sie return erreicht.Wenn eine Funktion das Schlüsselwort verwendet, handelt
yield
es sich um einen Generator.Jetzt können Sie folgendermaßen iterieren:
quelle
Pufferung. Wenn es effizient ist, Daten in großen Blöcken abzurufen, aber in kleinen Blöcken zu verarbeiten, kann ein Generator helfen:
Mit den obigen Anweisungen können Sie die Pufferung einfach von der Verarbeitung trennen. Die Consumer-Funktion kann jetzt nur die Werte einzeln abrufen, ohne sich um die Pufferung kümmern zu müssen.
quelle
Ich habe festgestellt, dass Generatoren sehr hilfreich sind, um Ihren Code zu bereinigen und Ihnen eine einzigartige Möglichkeit zu bieten, Code zu kapseln und zu modularisieren. In einer Situation, in der Sie etwas benötigen, um Werte basierend auf der eigenen internen Verarbeitung ständig auszuspucken, und wenn dieses Element von einer beliebigen Stelle in Ihrem Code (und nicht nur innerhalb einer Schleife oder eines Blocks) aufgerufen werden muss, sind Generatoren die Funktion dafür verwenden.
Ein abstraktes Beispiel wäre ein Fibonacci-Zahlengenerator, der nicht in einer Schleife lebt und bei einem Aufruf von überall immer die nächste Zahl in der Sequenz zurückgibt:
Jetzt haben Sie zwei Fibonacci-Nummerngeneratorobjekte, die Sie von überall in Ihrem Code aufrufen können, und sie geben immer größere Fibonacci-Nummern nacheinander wie folgt zurück:
Das Schöne an Generatoren ist, dass sie den Zustand einkapseln, ohne die Rahmen für die Erstellung von Objekten durchlaufen zu müssen. Eine Art, über sie nachzudenken, sind "Funktionen", die sich an ihren inneren Zustand erinnern.
Ich habe das Fibonacci-Beispiel von Python Generators erhalten - Was sind sie? Mit ein wenig Fantasie können Sie sich viele andere Situationen einfallen lassen, in denen Generatoren eine großartige Alternative zu
for
Schleifen und anderen traditionellen Iterationskonstrukten darstellen.quelle
Die einfache Erklärung: Betrachten Sie eine
for
AussageIn den meisten
iterable
Fällen müssen nicht alle Elemente von Anfang an vorhanden sein, sondern können bei Bedarf im laufenden Betrieb generiert werden. Dies kann in beiden Fällen viel effizienter seinIn anderen Fällen kennen Sie nicht einmal alle Elemente im Voraus. Beispielsweise:
Sie haben keine Möglichkeit, alle Befehle des Benutzers im Voraus zu kennen, aber Sie können eine schöne Schleife wie diese verwenden, wenn ein Generator Ihnen Befehle übergibt:
Mit Generatoren können Sie auch über unendliche Sequenzen iterieren, was beim Iterieren über Container natürlich nicht möglich ist.
quelle
itertool
- siehecycles
.Meine bevorzugten Anwendungen sind "Filtern" und "Reduzieren".
Angenommen, wir lesen eine Datei und möchten nur die Zeilen, die mit "##" beginnen.
Wir können dann die Generatorfunktion in einer richtigen Schleife verwenden
Das Reduzierungsbeispiel ist ähnlich. Angenommen, wir haben eine Datei, in der wir Zeilenblöcke suchen müssen
<Location>...</Location>
. [Keine HTML-Tags, sondern Zeilen, die tagartig aussehen.]Auch hier können wir diesen Generator in einer geeigneten for-Schleife verwenden.
Die Idee ist, dass eine Generatorfunktion es uns ermöglicht, eine Sequenz zu filtern oder zu reduzieren, indem jeweils eine andere Sequenz mit einem Wert erzeugt wird.
quelle
fileobj.readlines()
würde die gesamte Datei in eine Liste im Speicher lesen und den Zweck der Verwendung von Generatoren zunichte machen. Da Dateiobjekte bereits iterierbar sind, können Sie siefor b in your_generator(fileobject):
stattdessen verwenden. Auf diese Weise wird Ihre Datei zeilenweise gelesen, um das Lesen der gesamten Datei zu vermeiden.Ein praktisches Beispiel, bei dem Sie einen Generator verwenden könnten, ist, wenn Sie eine Form haben und über seine Ecken, Kanten oder was auch immer iterieren möchten. Für mein eigenes Projekt (Quellcode hier ) hatte ich ein Rechteck:
Jetzt kann ich ein Rechteck erstellen und seine Ecken durchlaufen:
Stattdessen
__iter__
könnten Sie eine Methode habeniter_corners
und diese mit aufrufenfor corner in myrect.iter_corners()
. Es ist nur eleganter zu verwenden,__iter__
da wir den Namen der Klasseninstanz direkt imfor
Ausdruck verwenden können.quelle
Grundsätzlich werden Rückruffunktionen vermieden, wenn über den Status der Eingabeerhaltung iteriert wird.
Siehe hier und hier für einen Überblick über die Generatoren getan werden kann.
quelle
Einige gute Antworten hier, ich würde jedoch auch empfehlen, das Tutorial zur funktionalen Programmierung von Python vollständig zu lesen, um einige der leistungsstärkeren Anwendungsfälle von Generatoren zu erklären.
quelle
Da die Sendemethode eines Generators nicht erwähnt wurde, ist hier ein Beispiel:
Es zeigt die Möglichkeit, einen Wert an einen laufenden Generator zu senden. Ein weiterführender Kurs zu Generatoren im folgenden Video (einschließlich
yield
Exploration, Generatoren für die Parallelverarbeitung, Überschreiten der Rekursionsgrenze usw.)David Beazley über Generatoren auf der PyCon 2014
quelle
Ich verwende Generatoren, wenn unser Webserver als Proxy fungiert:
quelle
Haufenweise Sachen. Jedes Mal, wenn Sie eine Folge von Elementen generieren möchten, diese aber nicht alle gleichzeitig in einer Liste "materialisieren" müssen. Sie könnten beispielsweise einen einfachen Generator haben, der Primzahlen zurückgibt:
Sie können dies dann verwenden, um die Produkte nachfolgender Primzahlen zu generieren:
Dies sind ziemlich triviale Beispiele, aber Sie können sehen, wie nützlich es sein kann, große (möglicherweise unendliche!) Datensätze zu verarbeiten, ohne sie vorher zu generieren, was nur eine der offensichtlicheren Anwendungen ist.
quelle
Auch zum Drucken der Primzahlen bis n geeignet:
quelle