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.
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.
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:
big_list = list(the_generator)
yield
und join
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:
# function version
def 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 version
def fibon(n):
a = b = 1
for 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.
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.
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 von generators
eingeführt und die yield
Anweisung dort fortgesetzt, wo das function
aufgehö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()
0
In [4]: gen.next()
1
In [5]: gen.next()
Das ist also der Unterschied zwischen return
und yield
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).
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 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:
def ResultGenerator(cursor, batchsize=1000):
while True:
results = cursor.fetchmany(batchsize)
if not results:
break
for result in results:
yield result
Wie Sie sehen können, behält unsere Funktion yield
die Ergebnisse bei. Wenn Sie das Schlüsselwort return
anstelle 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 yield
es 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 in ResultGenerator(cursor):
doSomethingWith(result)
db.close()
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():
while True:
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.
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 = 1
yield first
yield second
while 1:
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:
>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5
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.
Die einfache Erklärung: Betrachten Sie eine for
Aussage
for item in iterable:
do_stuff()
In 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 sein
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:
def user_input():
while True:
wait_for_command()
cmd = get_command()
yield cmd
Mit Generatoren können Sie auch über unendliche Sequenzen iterieren, was beim Iterieren über Container natürlich nicht möglich ist.
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= None
for line in aSequence:
if line.startswith("</Location"):
block.append( line )
yield block
block= None
keep= False
elif line.startsWith("<Location"):
block= [ line ]
keep= True
elif keep:
block.append( line )
else:
pass
if block is not None:
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.
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:
class Rect():
def __init__(self, x, y, width, height):
self.l_top = (x, y)
self.r_top = (x+width, y)
self.r_bot = (x+width, y+height)
self.l_bot = (x, y+height)
def __iter__(self):
yield self.l_top
yield self.r_top
yield self.r_bot
yield self.l_bot
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_corners
und diese mit aufrufen for corner in myrect.iter_corners()
. Es ist nur eleganter zu verwenden, __iter__
da wir den Namen der Klasseninstanz direkt im for
Ausdruck 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 = yield
print(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 yield
Exploration, 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)
yield 2
for i in itertools.count(1):
candidate = i * 2 + 1
if not 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.
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:
break
else:
yield(num)
for prime_num in genprime(100):
print(prime_num)