generator python Was macht das Keyword "Ertrag"?




15 Answers

Grokking zum Grokking yield

Wenn Sie eine Funktion mit yield , wenden Sie diesen einfachen Trick an, um zu verstehen, was passieren wird:

  1. Fügen Sie am Anfang der Funktion ein Zeilenergebnis result = [] .
  2. Ersetzen Sie jeden yield expr durch result.append(expr) .
  3. Fügen Sie ein Zeilenergebnis am Ende der Funktion ein.
  4. Ja, keine yield mehr! Code lesen und herausfinden.
  5. Funktion mit der ursprünglichen Definition vergleichen.

Dieser Trick vermittelt zwar einen Eindruck von der Logik der Funktion, aber was mit yield tatsächlich geschieht, unterscheidet sich erheblich von dem, was in der Liste vorkommt. In vielen Fällen wird der Ertragsansatz viel speichereffizienter und auch schneller sein. In anderen Fällen werden Sie durch diesen Trick in einer Endlosschleife hängen bleiben, obwohl die ursprüngliche Funktion einwandfrei funktioniert. Lesen Sie weiter, um mehr zu erfahren ...

Verwechsle nicht deine Iterables, Iteratoren und Generatoren

Zuerst das Iterator-Protokoll - wenn Sie schreiben

for x in mylist:
    ...loop body...

Python führt die folgenden zwei Schritte aus:

  1. Ruft einen Iterator für mylist :

    Aufruf iter(mylist) -> dies gibt ein Objekt mit einer next() Methode (oder __next__() in Python 3) zurück.

    [Dies ist der Schritt, den die meisten Leute vergessen, um Ihnen zu erzählen]

  2. Verwendet den Iterator, um Elemente zu durchlaufen:

    Rufen Sie die next() -Methode weiterhin auf dem von Schritt 1 zurückgegebenen Iterator auf. Der Rückgabewert von next() wird x zugewiesen und der Schleifenkörper wird ausgeführt. Wenn eine Ausnahme StopIteration innerhalb von next() StopIteration wird, bedeutet dies, dass der Iterator keine weiteren Werte enthält und die Schleife beendet wird.

Die Wahrheit ist, dass Python die oben genannten zwei Schritte immer dann ausführt, wenn es den Inhalt eines Objekts durchlaufen möchte - es könnte sich also um eine for-Schleife handeln. Es könnte sich jedoch auch um Code handeln, der wie otherlist.extend(mylist) (wo eine Liste eine Python-Liste ist) .

Hier ist mylist eine Iteration, weil sie das Iterator-Protokoll implementiert. In einer benutzerdefinierten Klasse können Sie die __iter__() Methode implementieren, um Instanzen Ihrer Klasse iterierbar zu machen. Diese Methode sollte einen Iterator zurückgeben . Ein Iterator ist ein Objekt mit einer next() Methode. Es ist möglich, sowohl __iter__() als auch next() in derselben Klasse zu implementieren und __iter__() self . Dies funktioniert in einfachen Fällen, aber nicht, wenn zwei Iteratoren gleichzeitig über dasselbe Objekt laufen sollen.

Das ist das Iterator-Protokoll. Viele Objekte implementieren dieses Protokoll:

  1. Eingebaute Listen, Wörterbücher, Tupel, Sets, Dateien.
  2. Benutzerdefinierte Klassen, die __iter__() implementieren.
  3. Generatoren.

Beachten Sie, dass eine for Schleife nicht weiß, um welche Art von Objekt es sich handelt - sie folgt lediglich dem Iteratorprotokoll und ist froh, wenn Sie item (nach dem next() . Integrierte Listen geben ihre Elemente einzeln zurück, Wörterbücher geben die Tasten einzeln zurück, Dateien geben die Zeilen einzeln wieder usw. zurück. Und Generatoren kehren zurück.

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Wenn Sie in f123() drei return Anweisungen hatten, f123() nur die erste ausgeführt und die Funktion beendet. Aber f123() ist keine gewöhnliche Funktion. Wenn f123() aufgerufen wird, gibt es keinen der Werte in den Yield-Anweisungen zurück! Es gibt ein Generatorobjekt zurück. Die Funktion wird auch nicht wirklich beendet - sie wechselt in den Suspend-Zustand. Wenn die for Schleife versucht, das Generator-Objekt zu überlaufen, wird die Funktion nach der zuvor zurückgegebenen yield bereits in der nächsten Zeile aus dem angehaltenen Zustand wieder aufgenommen, führt die nächste Codezeile aus, in diesem Fall eine yield Anweisung, und gibt diese als zurück der nächste Punkt Dies geschieht so lange, bis die Funktion beendet ist. An diesem Punkt hebt der Generator die StopIteration an und die Schleife wird beendet.

Das Generator-Objekt ist also __iter__() ein Adapter - an einem Ende weist es das Iterator-Protokoll auf, indem es die __iter__() und next() __iter__() , um die for Schleife glücklich zu machen. Am anderen Ende führt es die Funktion jedoch gerade genug aus, um den nächsten Wert herauszuholen, und versetzt ihn in den Suspend-Modus.

Warum Generatoren verwenden?

Normalerweise können Sie Code schreiben, der keine Generatoren verwendet, aber dieselbe Logik implementiert. Eine Option ist die Verwendung der temporären Liste "Trick", die ich zuvor erwähnt habe. Dies funktioniert nicht in allen Fällen, z. B. wenn Sie Endlosschleifen haben, oder es nutzt den Speicher möglicherweise ineffizient, wenn Sie eine sehr lange Liste haben. Der andere Ansatz besteht darin, eine neue iterable-Klasse SomethingIter zu implementieren, die den Status in Instanzmitgliedern behält und den nächsten logischen Schritt in ihrer next() (oder __next__() in Python 3) -Methode __next__() . Je nach Logik kann der Code in der next() -Methode sehr komplex aussehen und anfällig für Fehler sein. Hier bieten Generatoren eine saubere und einfache Lösung.

python iterator class

Was ist die Verwendung des yield Keyword in Python? Was tut es?

Zum Beispiel versuche ich, diesen Code 1 zu verstehen:

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

Und das ist der Anrufer:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Was passiert, wenn die Methode _get_child_candidates aufgerufen wird? Wird eine Liste zurückgegeben? Ein einzelnes Element? Heißt es schon wieder? Wann werden nachfolgende Anrufe aufhören?

1. Der Code stammt von Jochen Schulz (jrschulz), der eine großartige Python-Bibliothek für metrische Bereiche erstellt hat. Dies ist der Link zur vollständigen Quelle: Modul mspace .




Das yield Schlüsselwort wird auf zwei einfache Fakten reduziert:

  1. Wenn der Compiler das ertragswortschlüsselwort irgendwo in einer Funktion erkennt, wird diese Funktion nicht mehr über die return Anweisung zurückgegeben. Stattdessen wird sofort ein Lazy "Pending List" -Objekt namens Generator zurückgegeben
  2. Ein Generator ist iterierbar. Was ist ein Iterer ? Es ist alles wie eine list set oder eine range oder eine Diktieransicht mit einem integrierten Protokoll, mit dem jedes Element in einer bestimmten Reihenfolge aufgerufen werden kann .

Kurz gesagt: Ein Generator ist eine faule, inkrementell anhängige Liste , und mit yield können Sie die Listenwerte mit Funktionsschreibweise programmieren, die der Generator inkrementell ausspucken sollte.

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

Beispiel

Definieren Sie eine Funktion makeRange , die der Python- range . makeRange(n) aufrufen Ruft einen Generator zurück:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

Um zu erzwingen, dass der Generator die anstehenden Werte sofort zurückgibt, können Sie ihn in list() (so wie Sie es auch iterierbar machen könnten):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

Beispiel vergleichen mit "nur eine Liste zurückgeben"

Das obige Beispiel kann als bloßes Erstellen einer Liste betrachtet werden, an die Sie anhängen und zurückkehren:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

Es gibt jedoch einen wesentlichen Unterschied. siehe den letzten Abschnitt.

Wie können Sie Generatoren verwenden?

Ein iterierbares Element ist der letzte Teil eines Listenverständnisses. Alle Generatoren sind iterierbar. Daher werden sie häufig wie folgt verwendet:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

Um ein besseres Gefühl für Generatoren zu erhalten, können Sie mit dem itertools Modul itertools (verwenden Sie chain.from_iterable anstelle von chain wenn dies erforderlich ist). Beispielsweise können Sie Generatoren verwenden, um unendlich lange Lazy-Listen wie itertools.count() zu implementieren. Sie können Ihr eigenes def enumerate(iterable): zip(count(), iterable) implementieren def enumerate(iterable): zip(count(), iterable) oder alternativ mit dem yield Schlüsselwort in einer while-Schleife.

Bitte beachten Sie: Generatoren können tatsächlich für viel mehr Dinge verwendet werden, z. B. für die Implementierung von Coroutinen oder für nicht deterministische Programmierung oder andere elegante Dinge. Die von mir hier vorgestellte Ansicht "Lazy-Listen" ist jedoch die häufigste Verwendung, die Sie finden werden.

Hinter den Kulissen

So funktioniert das "Python-Iterationsprotokoll". Das ist, was list(makeRange(5)) wenn Sie eine list(makeRange(5)) . Ich beschreibe das früher als "faule, inkrementelle Liste".

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Die eingebaute Funktion next() ruft einfach die .next() Funktion auf, die Teil des "Iterationsprotokolls" ist und auf allen Iteratoren vorhanden ist. Sie können die next() Funktion (und andere Teile des Iterationsprotokolls) manuell verwenden, um ausgefallene Dinge zu implementieren, normalerweise auf Kosten der Lesbarkeit. Versuchen Sie daher, dies zu vermeiden.

Minutien

Normalerweise würden sich die meisten Leute nicht für die folgenden Unterschiede interessieren und würden wahrscheinlich hier aufhören zu lesen.

In Python-Sprache ist ein Iterer ein Objekt, das "das Konzept einer for-Schleife" wie eine Liste " [1,2,3] , und ein Iterator ist eine bestimmte Instanz der angeforderten for-Schleife wie [1,2,3].__iter__() . Ein Generator ist genau dasselbe wie jeder andere Iterator, abgesehen von der Art und Weise, wie er geschrieben wurde (mit Funktionssyntax).

Wenn Sie einen Iterator aus einer Liste anfordern, wird ein neuer Iterator erstellt. Wenn Sie jedoch einen Iterator von einem Iterator anfordern (was Sie selten tun würden), erhalten Sie nur eine Kopie von sich selbst.

In dem unwahrscheinlichen Fall, dass Sie so etwas nicht tun ...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... dann denken Sie daran, dass ein Generator ein Iterator ist ; das heißt, es ist einmalig. Wenn Sie es wiederverwenden möchten, müssen Sie myRange(...) erneut aufrufen. Wenn Sie das Ergebnis zweimal verwenden müssen, konvertieren Sie das Ergebnis in eine Liste und speichern Sie es in einer Variablen x = list(myRange(5)) . Diejenigen, die unbedingt einen Generator klonen müssen (z. B. furchterregende Metaprogrammierung), können itertools.tee wenn dies unbedingt erforderlich ist, da der kopierbare Iterator Python PEP Standardvorschlag zurückgestellt wurde.




yieldist wie return- es gibt alles zurück, wozu man es sagt (als Generator). Der Unterschied ist, dass beim nächsten Aufruf des Generators die Ausführung mit dem letzten Aufruf der yieldAnweisung beginnt . Im Gegensatz zu return wird der Stack-Frame nicht gesäubert, wenn eine Rendite auftritt. Die Kontrolle wird jedoch an den Aufrufer zurückgegeben. Der Zustand wird also beim nächsten Mal wieder aktiviert.

Bei Ihrem Code get_child_candidatesverhält sich die Funktion wie ein Iterator, so dass Sie beim Erweitern Ihrer Liste der neuen Liste jeweils ein Element hinzufügen.

list.extendruft einen Iterator auf, bis er erschöpft ist. Im Falle des von Ihnen geposteten Codebeispiels wäre es viel klarer, einfach ein Tupel zurückzugeben und dieses an die Liste anzufügen.




Für diejenigen, die ein minimales Arbeitsbeispiel bevorzugen, meditieren Sie über diese interaktive Python Sitzung:

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print i
... 
1
2
3
>>> for i in g:
...   print i
... 
>>> # Note that this time nothing was printed



TL; DR

An Stelle von:

def squares_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

mach das:

def squares_the_yield_way(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Immer wenn Sie eine Liste von Grund auf neu erstellen, finden Sie yieldstattdessen jedes Stück.

Dies war mein erster "Aha" -Moment mit Ertrag.

yield ist eine zuckersüße Art zu sagen

Baue eine Reihe von Sachen

Gleiches Verhalten:

>>> for square in squares_list(4):
...     print(square)
...
0
1
4
9
>>> for square in squares_the_yield_way(4):
...     print(square)
...
0
1
4
9

Anderes Verhalten:

Die Ausbeute ist ein Durchlauf : Sie können nur einmal durchlaufen. Wenn eine Funktion einen Ertrag enthält, nennen wir sie eine Generatorfunktion . Und ein iterator ist, was er zurückgibt. Das ist aufschlussreich. Wir verlieren den Komfort eines Containers, gewinnen aber die Macht einer beliebig langen Serie.

Die Rendite ist faul , die Berechnung wird verschoben. Eine Funktion mit einer Rendite wird beim Aufruf überhaupt nicht ausgeführt . Das zurückgegebene Iteratorobjekt verwendet magic , um den internen Kontext der Funktion zu erhalten. Bei jedem Aufruf next()des Iterators (dies geschieht in einer for-Schleife) wird die Ausführung in Zentimeter bis zum nächsten Ertrag fortgesetzt. ( returnhebt StopIterationund beendet die Serie.)

Der Ertrag ist vielseitig . Es können unendlich viele Schleifen ausgeführt werden:

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

Wenn Sie mehrere Durchgänge benötigen und die Serie nicht zu lang ist, rufen list()Sie einfach an:

>>> list(squares_the_yield_way(4))
[0, 1, 4, 9]

Brillante Wahl des Wortes, yieldweil beide Bedeutungen zutreffen:

Ertrag - produzieren oder liefern (wie in der Landwirtschaft)

... liefern die nächsten Daten der Serie.

nachgeben - nachgeben oder aufgeben (wie in politischer Macht)

... verzichten auf die CPU-Ausführung, bis der Iterator fortschreitet.




Es gibt eine Art von Antwort, die meiner Meinung nach noch nicht gegeben wurde, unter den vielen großartigen Antworten, die beschreiben, wie Generatoren verwendet werden. Hier ist die Antwort auf die Programmiersprachentheorie:

Die yieldAnweisung in Python gibt einen Generator zurück. Ein Generator in Python ist eine Funktion, die Fortsetzungen zurückgibt (und insbesondere eine Art Coroutine, Fortsetzungen stellen jedoch den allgemeineren Mechanismus dar, um zu verstehen, was vor sich geht).

Fortsetzungen in der Theorie der Programmiersprachen sind eine wesentlich grundlegendere Art der Berechnung, sie werden jedoch nicht oft verwendet, da sie extrem schwer zu verstehen und auch sehr schwer zu implementieren sind. Aber die Vorstellung von einer Fortsetzung ist unkompliziert: Es ist der Zustand einer Berechnung, der noch nicht abgeschlossen ist. In diesem Zustand werden die aktuellen Werte der Variablen, die noch auszuführenden Operationen usw. gespeichert. Später im Programm kann dann die Fortsetzung aufgerufen werden, so dass die Programmvariablen auf diesen Zustand zurückgesetzt werden und die gespeicherten Operationen ausgeführt werden.

Fortsetzungen in dieser allgemeineren Form können auf zwei Arten implementiert werden. In der call/ccArt und Weise wird der Programmstapel buchstäblich gespeichert, und wenn die Fortsetzung aufgerufen wird, wird der Stapel wiederhergestellt.

In Continuation Passing Style (CPS) sind Fortsetzungen normale Funktionen (nur in Sprachen, in denen Funktionen erstklassig sind), die der Programmierer explizit verwaltet und an Unterprogramme weitergibt. In diesem Stil wird der Programmstatus durch Schließungen (und die darin verschlüsselten Variablen) und nicht durch irgendwo auf dem Stapel befindliche Variablen dargestellt. Funktionen, die den Steuerfluss verwalten, akzeptieren die Fortsetzung als Argumente (in einigen Variationen von CPS akzeptieren Funktionen möglicherweise mehrere Fortsetzungen) und manipulieren den Steuerfluss, indem sie aufgerufen werden, indem sie einfach aufgerufen und anschließend zurückgegeben werden. Ein sehr einfaches Beispiel für einen Weiterleitungsstil ist:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

In diesem (sehr simplen) Beispiel speichert der Programmierer das eigentliche Schreiben der Datei in einer Fortsetzung (was möglicherweise eine sehr komplexe Operation mit vielen zu schreibenden Details sein kann) und übergibt dann diese Fortsetzung (dh als erstes). Klassenabschluss) an einen anderen Operator, der einige weitere Verarbeitungsschritte ausführt und diese dann bei Bedarf aufruft. (Ich verwende dieses Entwurfsmuster häufig in der eigentlichen GUI-Programmierung, entweder weil mir Codezeilen erspart werden oder, was noch wichtiger ist, der Steuerungsfluss nach dem Auslösen von GUI-Ereignissen verwaltet wird.)

Der Rest dieses Beitrags wird Fortsetzungen ohne Verlust der Allgemeinheit als CPS konzeptualisieren, da er viel einfacher zu verstehen und zu lesen ist.


Sprechen wir jetzt über Generatoren in Python. Generatoren sind ein spezifischer Subtyp der Fortsetzung. Während Fortsetzungen im Allgemeinen den Zustand einer Berechnung (dh den Aufrufstack des Programms) speichern können , können Generatoren nur den Iterationszustand über einen Iterator speichern . Obwohl diese Definition für bestimmte Anwendungsfälle von Generatoren etwas irreführend ist. Zum Beispiel:

def f():
  while True:
    yield 4

Dies ist eindeutig ein vernünftiges iterierbares Verhalten, dessen Verhalten genau definiert ist - jedes Mal, wenn der Generator über sie iteriert, gibt er 4 zurück (und dies für immer). Es ist jedoch wahrscheinlich nicht der prototypische Typ von Iterer, der mir einfällt, wenn ich an Iteratoren (dh for x in collection: do_something(x)) denke . Dieses Beispiel veranschaulicht die Leistung von Generatoren: Wenn etwas ein Iterator ist, kann ein Generator den Zustand seiner Iteration speichern.

Wiederholen: Fortsetzungen können den Status des Programmstapels speichern, und Generatoren können den Iterationsstatus speichern. Das bedeutet, dass Fortsetzungen wesentlich leistungsfähiger sind als Generatoren, aber auch Generatoren sind viel einfacher. Sie sind für den Sprachdesigner leichter zu implementieren und für den Programmierer einfacher zu verwenden (wenn Sie etwas Zeit zum Brennen haben, versuchen Sie, diese Seite mit den Fortsetzungen und dem Aufruf / cc zu lesen und zu verstehen ).

Sie können Generatoren jedoch leicht als einfachen, spezifischen Fall des Weiterleitungs-Übergabestils implementieren (und konzeptualisieren):

Bei jedem yieldAufruf wird die Funktion aufgefordert, eine Fortsetzung zurückzugeben. Wenn die Funktion erneut aufgerufen wird, beginnt sie dort, wo sie aufgehört hat. Im Pseudo-Pseudocode (dh nicht im Pseudocode, aber nicht im Code) nextlautet die Methode des Generators grundsätzlich wie folgt:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

wobei das yieldSchlüsselwort eigentlich syntaktischer Zucker für die echte Generatorfunktion ist, im Grunde so etwas wie:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

Denken Sie daran, dass dies nur ein Pseudocode ist und die tatsächliche Implementierung von Generatoren in Python komplexer ist. Um zu verstehen, was los ist, versuchen Sie, einen Weiterleitungsstil zu verwenden, um Generatorobjekte ohne Verwendung des yieldSchlüsselworts zu implementieren .




Während viele Antworten zeigen, warum Sie yieldeinen Generator zum Erstellen eines Generators verwenden, gibt es mehr Möglichkeiten für yield. Es ist ziemlich einfach, eine Coroutine zu erstellen, die die Weitergabe von Informationen zwischen zwei Codeblöcken ermöglicht. Ich werde keines der schönen Beispiele wiederholen, die bereits für die yieldErstellung eines Generators genannt wurden.

Um zu verstehen, was a yieldim folgenden Code bewirkt, können Sie den Zyklus durch einen beliebigen Code mit einem Finger verfolgen yield. Jedes Mal, wenn Ihr Finger auf den Finger schlägt yield, müssen Sie warten, bis ein nextoder ein sendEintrag erfolgt. Wenn a nextaufgerufen wird, verfolgen Sie den Code, bis Sie auf yield... klicken. Der Code rechts von yieldwird ausgewertet und an den Anrufer zurückgegeben. Dann warten Sie. Wenn nexterneut aufgerufen wird, führen Sie eine weitere Schleife durch den Code. Sie werden jedoch feststellen, dass in einer Coroutine yieldauch eine send… verwendet werden kann, die einen Wert vom Aufrufer in die nachgiebige Funktion sendet . Wenn a sendgegeben ist, dannyieldempfängt den gesendeten Wert und spuckt ihn auf der linken Seite aus… dann wird der Trace durch den Code fortgesetzt, bis Sie erneut auf die yieldSchaltfläche drücken (den Wert am Ende zurückgeben, als ob er nextaufgerufen wurde).

Zum Beispiel:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()



Ich wollte "lesen Seite 19 von Beazleys" Python: Essential Reference "für eine schnelle Beschreibung der Generatoren", aber so viele andere haben bereits gute Beschreibungen veröffentlicht.

Beachten Sie auch, dass yieldin Coroutinen das Doppelte ihrer Verwendung in Generatorfunktionen verwendet werden kann. Obwohl es nicht dieselbe Verwendung wie Ihr Code-Snippet ist, (yield)kann es als Ausdruck in einer Funktion verwendet werden. Wenn ein Aufrufer mit der send()Methode einen Wert an die Methode sendet , wird die Coroutine ausgeführt, bis die nächste (yield)Anweisung gefunden wird.

Generatoren und Coroutinen sind eine coole Methode, um Datenflussanwendungen einzurichten. Ich dachte, es würde sich lohnen, die andere Verwendung der yieldAnweisung in Funktionen zu kennen.




Aus Programmiersicht werden die Iteratoren als thunks implementiert .

Um Iteratoren, Generatoren und Thread-Pools für die gleichzeitige Ausführung usw. als Thunks (auch als anonyme Funktionen bezeichnet) zu implementieren, verwendet man Nachrichten, die an ein Abschlussobjekt gesendet werden, das über einen Dispatcher verfügt, und der Dispatcher antwortet auf "Messages".

http://en.wikipedia.org/wiki/Message_passing

" next " ist eine Nachricht, die an eine Schließung gesendet wird, die durch den Aufruf " iter " erstellt wurde.

Es gibt viele Möglichkeiten, diese Berechnung zu implementieren. Ich habe die Mutation verwendet, aber es ist leicht, dies ohne Mutation zu tun, indem der aktuelle Wert und der nächste Yielder zurückgegeben werden.

Hier ist eine Demonstration, die die Struktur von R6RS verwendet, aber die Semantik ist absolut identisch mit der von Python. Es ist dasselbe Berechnungsmodell und nur eine Änderung der Syntax ist erforderlich, um es in Python neu zu schreiben.

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->



Hier ist ein einfaches Beispiel:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

Ausgabe:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

Ich bin kein Python-Entwickler, aber es scheint mir yielddie Position des Programmflusses zu halten und die nächste Schleife beginnt von der Position "Rendite". Es scheint, als würde es an dieser Position warten, und kurz davor einen Wert nach außen zurückgeben, und das nächste Mal funktioniert es weiter.

Es scheint eine interessante und nette Fähigkeit zu sein: D




Wie jede Antwort vermuten lässt, yieldwird ein Sequenzgenerator erstellt. Es wird zum dynamischen Erzeugen einer Sequenz verwendet. Wenn Sie z. B. eine Datei zeilenweise in einem Netzwerk lesen, können Sie die yieldFunktion wie folgt verwenden:

def getNextLines():
   while con.isOpen():
       yield con.read()

Sie können es in Ihrem Code wie folgt verwenden:

for line in getNextLines():
    doSomeThing(line)

Ausführungskontrolltransfer gotcha

Die Ausführungssteuerung wird von getNextLines () an die forSchleife übergeben, wenn Yield ausgeführt wird. Jedes Mal, wenn getNextLines () aufgerufen wird, beginnt die Ausführung an dem Punkt, an dem sie zuletzt angehalten wurde.

Also eine Funktion mit folgendem Code

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

wird drucken

"first time"
"second time"
"third time"
"Now some useful value 12"



Zusammenfassend, die yieldAnweisung wandelt Ihre Funktion in eine Factory um, die ein spezielles Objekt erzeugt, das als a bezeichnet wird generatorund sich um den Körper Ihrer ursprünglichen Funktion legt. Wenn generatorit wiederholt wird, führt es Ihre Funktion aus, bis es die nächste erreicht, setzt yielddann die Ausführung aus und wertet den übergebenen Wert aus yield. Dieser Vorgang wird bei jeder Wiederholung wiederholt, bis der Ausführungspfad die Funktion beendet. Zum Beispiel,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

einfach ausgänge

one
two
three

Die Leistung kommt von der Verwendung des Generators mit einer Schleife, die eine Sequenz berechnet. Der Generator führt die Schleife jedes Mal aus, um das nächste Ergebnis der Berechnung zu "erbringen". Auf diese Weise wird eine Liste "on the fly" berechnet für besonders große Berechnungen gespeichert

Angenommen, Sie wollten eine eigene rangeFunktion erstellen , die einen iterierbaren Zahlenbereich erzeugt. Sie könnten es wie folgt tun:

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

und benutze es so;

for i in myRangeNaive(10):
    print i

Dies ist aber ineffizient, weil

  • Sie erstellen ein Array, das Sie nur einmal verwenden (dadurch wird Speicherplatz verschwendet).
  • Dieser Code durchläuft dieses Array tatsächlich zweimal! :(

Glücklicherweise waren Guido und sein Team großzügig genug, um Generatoren zu entwickeln, sodass wir dies einfach tun konnten.

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

Bei jeder Iteration führt nun eine aufgerufene Funktion des Generators next()die Funktion aus, bis sie entweder eine 'Yield'-Anweisung erreicht, in der sie stoppt und den Wert' nachgibt 'oder das Ende der Funktion erreicht. In diesem Fall wird beim ersten Aufruf next()bis zur Ertragsaussage und Rendite 'n' ausgeführt, beim nächsten Aufruf wird die Inkrementierungsaussage ausgeführt, zum 'while' zurück gesprungen, ausgewertet, und wenn wahr, wird er beendet und Geben Sie 'n' wieder ein, wird es so fortgesetzt, bis die while-Bedingung den Wert false ergibt und der Generator zum Ende der Funktion springt.




(Meine Antwort unten bezieht sich nur auf die Verwendung des Python-Generators, nicht auf die zugrunde liegende Implementierung des Generatormechanismus , der einige Tricks der Stapel- und Heap-Manipulation beinhaltet.)

Wenn yieldanstelle von returnin einer Python-Funktion verwendet wird, wird diese Funktion in einen speziellen Aufruf umgewandelt generator function. Diese Funktion gibt ein Objekt vom generatorTyp zurück. Das yieldSchlüsselwort ist ein Flag, um den Python-Compiler zu benachrichtigen, um eine solche Funktion speziell zu behandeln. Normale Funktionen werden beendet, sobald ein Wert zurückgegeben wird. Mit Hilfe des Compilers kann die Generatorfunktion jedoch als wiederaufgenommen betrachtet werden. Das heißt, der Ausführungskontext wird wiederhergestellt und die Ausführung wird ab der letzten Ausführung fortgesetzt. Bis Sie explizit return aufrufen, wird eine StopIterationAusnahme ausgelöst (die auch Teil des Iterator-Protokolls ist) oder das Ende der Funktion erreicht. Ich fand eine Menge von Referenzen über , generatoraber diese onevon dem functional programming perspectiveist das verdaulichste.

(Jetzt möchte ich über die Beweggründe sprechen generator, und das iteratorbasiert auf meinem eigenen Verständnis. Ich hoffe , das Sie erfassen helfen können wesentliche Motivation der Iterator und Generator. Ein solches Konzept zeigt sich auch in anderen Sprachen auf wie C #) .

Ich verstehe, wenn wir ein Bündel von Daten verarbeiten wollen, speichern wir die Daten normalerweise erst irgendwo und verarbeiten sie dann einzeln. Dieser intuitive Ansatz ist jedoch problematisch. Wenn das Datenvolumen sehr groß ist, ist es teuer, sie zuvor als Ganzes zu speichern. Also, anstatt sich dataselbst direkt zu speichern, warum nicht irgendeine Art metadataindirekt speichern , dhthe logic how the data is computed .

Es gibt zwei Ansätze, um solche Metadaten einzuwickeln.

  1. Beim OO-Ansatz wickeln wir die Metadaten ein as a class. Dies ist der sogenannte iteratorwho, der das Iterator-Protokoll (dh die Methoden __next__()und und __iter__()) implementiert . Dies ist auch das häufig gesehene Iterator-Entwurfsmuster .
  2. Beim funktionalen Ansatz wickeln wir die Metadaten ein as a function. Dies ist das sogenannte generator function. Aber unter der Haube, die zurück generator objectnoch IS-Aiterator , weil es auch das Iterator - Protokoll implementiert.

In jedem Fall wird ein Iterator erstellt, dh ein Objekt, das Ihnen die gewünschten Daten liefern kann. Der OO-Ansatz kann etwas komplex sein. Wie auch immer, welches Sie verwenden, liegt bei Ihnen.




Das yieldKeyword sammelt lediglich die zurückgegebenen Ergebnisse. Denken Sie an yieldwiereturn +=




Noch ein TL; DR

Iterator in Liste : next()Gibt das nächste Element der Liste zurück

Iterator-Generator : next()berechnet das nächste Element (Code ausführen)

Sie können den Ertrag / Generator als eine Möglichkeit sehen, den Kontrollfluss von außen manuell auszuführen (z. B. eine Schleife weiter zu führen), indem Sie nextden Flow unabhängig von der Komplexität aufrufen .

Hinweis : Der Generator ist KEINE normale Funktion. Es speichert den vorherigen Zustand wie lokale Variablen (Stack). Weitere Erklärungen finden Sie in anderen Antworten oder Artikeln. Der Generator kann nur einmal wiederholt werden . Sie könnten ohne auskommen yield, aber es wäre nicht so schön, daher kann es als "sehr schöner" Sprachzucker betrachtet werden.




Related