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.

Quamrana
quelle
6
Vielleicht wäre eine bessere Frage, wann wir 'em
cregox
1
Beispiel aus der realen Welt hier
Giri

Antworten:

239

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)
Thomas Wouters
quelle
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 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.

nosklo
quelle
18
und wenn Sie eine Liste benötigen, können Sie immer tunlist(fibon(5))
Endolith
41

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.

Nickolay
quelle
1
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()
    0
    In [4]: gen.next()
    1
    In [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).

Fata Morgana
quelle
33

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 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:

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 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 in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()
Giri
quelle
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():
  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.

Rafał Dowgird
quelle
3
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 = 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 forSchleifen und anderen traditionellen Iterationskonstrukten darstellen.

Andz
quelle
19

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:

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.

dF.
quelle
... 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= 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.

S.Lott
quelle
8
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:

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

Pithikos
quelle
Ich liebte die Idee, ähnliche Klassenfelder als Generator zu übergeben
eusoubrasileiro
7

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.

MvdD
quelle
4

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.

Songololo
quelle
3

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 yieldExploration, Generatoren für die Parallelverarbeitung, Überschreiten der Rekursionsgrenze usw.)

David Beazley über Generatoren auf der PyCon 2014

John Damen
quelle
2

Ich verwende Generatoren, wenn unser Webserver als Proxy fungiert:

  1. Der Client fordert eine Proxy-URL vom Server an
  2. Der Server beginnt mit dem Laden der Ziel-URL
  3. Der Server gibt nach, um die Ergebnisse an den Client zurückzugeben, sobald er sie erhält
Brian
quelle
1

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.

Nick Johnson
quelle
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:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
Sébastien Wieckowski
quelle