Was macht das Schlüsselwort "Yield"?


10194

Was ist die Verwendung des yieldSchlüsselworts in Python und was macht 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_candidatesaufgerufen wird? Wird eine Liste zurückgegeben? Ein einzelnes Element? Wird es wieder aufgerufen? Wann werden nachfolgende Anrufe beendet?


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

Antworten:


14645

Um zu verstehen, was yieldfunktioniert, müssen Sie verstehen, was Generatoren sind. Und bevor Sie Generatoren verstehen können, müssen Sie iterables verstehen .

Iterables

Wenn Sie eine Liste erstellen, können Sie deren Elemente einzeln lesen. Das Lesen der Elemente nacheinander wird als Iteration bezeichnet:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylistist eine iterable . Wenn Sie ein Listenverständnis verwenden, erstellen Sie eine Liste und damit eine iterierbare:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

Alles, wofür Sie " for... in..." verwenden können, ist iterierbar. lists, strings, Dateien ...

Diese iterablen Dateien sind praktisch, da Sie sie beliebig oft lesen können, aber alle Werte im Speicher speichern. Dies ist nicht immer das, was Sie möchten, wenn Sie viele Werte haben.

Generatoren

Generatoren sind Iteratoren, eine Art Iterierbarkeit, die Sie nur einmal durchlaufen können . Generatoren speichern nicht alle Werte im Speicher, sie generieren die Werte im laufenden Betrieb :

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

Es ist genau das gleiche, außer dass Sie ()anstelle von verwendet haben []. ABER Sie können keinfor i in mygenerator zweites Mal ausführen, da Generatoren nur einmal verwendet werden können: Sie berechnen 0, vergessen es dann und berechnen 1 und beenden die Berechnung von 4 nacheinander.

Ausbeute

yieldist ein Schlüsselwort, das wie folgt verwendet wird return, außer dass die Funktion einen Generator zurückgibt.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

Hier ist es ein nutzloses Beispiel, aber es ist praktisch, wenn Sie wissen, dass Ihre Funktion eine große Anzahl von Werten zurückgibt, die Sie nur einmal lesen müssen.

Um zu beherrschen yield, müssen Sie verstehen, dass beim Aufrufen der Funktion der Code, den Sie in den Funktionskörper geschrieben haben, nicht ausgeführt wird. Die Funktion gibt nur das Generatorobjekt zurück, das ist etwas knifflig :-)

Dann wird Ihr Code bei jeder forVerwendung des Generators dort fortgesetzt, wo er aufgehört hat.

Nun der schwierige Teil:

forWenn Sie das aus Ihrer Funktion erstellte Generatorobjekt zum ersten Mal aufrufen, wird der Code in Ihrer Funktion von Anfang an ausgeführt, bis er trifft yield. Anschließend wird der erste Wert der Schleife zurückgegeben. Dann führt jeder nachfolgende Aufruf eine weitere Iteration der Schleife aus, die Sie in die Funktion geschrieben haben, und gibt den nächsten Wert zurück. Dies wird fortgesetzt, bis der Generator als leer betrachtet wird. Dies geschieht, wenn die Funktion ohne Treffer ausgeführt wird yield. Das kann daran liegen, dass die Schleife beendet ist oder dass Sie eine nicht mehr erfüllen "if/else".


Ihr Code erklärt

Generator:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

Anrufer:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

Dieser Code enthält mehrere intelligente Teile:

  • Die Schleife iteriert in einer Liste, aber die Liste wird erweitert, während die Schleife iteriert wird :-) Dies ist eine übersichtliche Methode, um alle diese verschachtelten Daten zu durchlaufen, auch wenn dies etwas gefährlich ist, da Sie möglicherweise eine Endlosschleife erhalten. In diesem Fall candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))erschöpfen Sie alle Werte des Generators, whileerstellen jedoch weiterhin neue Generatorobjekte, die andere Werte als die vorherigen erzeugen, da sie nicht auf denselben Knoten angewendet werden.

  • Die extend()Methode ist eine Listenobjektmethode, die eine Iterierbarkeit erwartet und ihre Werte zur Liste hinzufügt.

Normalerweise übergeben wir ihm eine Liste:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

Aber in Ihrem Code bekommt es einen Generator, was gut ist, weil:

  1. Sie müssen die Werte nicht zweimal lesen.
  2. Möglicherweise haben Sie viele Kinder und möchten nicht, dass alle im Speicher gespeichert werden.

Und es funktioniert, weil es Python egal ist, ob das Argument einer Methode eine Liste ist oder nicht. Python erwartet Iterables, damit es mit Zeichenfolgen, Listen, Tupeln und Generatoren funktioniert! Dies wird als Ententypisierung bezeichnet und ist einer der Gründe, warum Python so cool ist. Aber das ist eine andere Geschichte, für eine andere Frage ...

Sie können hier anhalten oder ein wenig lesen, um eine erweiterte Verwendung eines Generators zu sehen:

Steuerung einer Generatorerschöpfung

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

Hinweis: Verwenden Sie für Python 3 print(corner_street_atm.__next__())oderprint(next(corner_street_atm))

Dies kann für verschiedene Zwecke nützlich sein, z. B. zum Steuern des Zugriffs auf eine Ressource.

Itertools, dein bester Freund

Das Modul itertools enthält spezielle Funktionen zum Bearbeiten von Iterables. Möchten Sie jemals einen Generator duplizieren? Kette zwei Generatoren? Werte in einer verschachtelten Liste mit einem Einzeiler gruppieren? Map / Zipohne eine andere Liste zu erstellen?

Dann einfach import itertools.

Ein Beispiel? Sehen wir uns die möglichen Ankunftsreihenfolgen für ein Vierpferderennen an:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

Die inneren Mechanismen der Iteration verstehen

Iteration ist ein Prozess, der Iterables (Implementierung der __iter__()Methode) und Iteratoren (Implementierung der __next__()Methode) impliziert . Iterables sind alle Objekte, von denen Sie einen Iterator erhalten können. Iteratoren sind Objekte, mit denen Sie iterable iterieren können.

In diesem Artikel erfahren Sie mehr darüber, wie forSchleifen funktionieren .


355
yieldist nicht so magisch, wie diese Antwort nahelegt. Wenn Sie eine Funktion aufrufen, die yieldirgendwo eine Anweisung enthält , erhalten Sie ein Generatorobjekt, aber es wird kein Code ausgeführt. Jedes Mal, wenn Sie ein Objekt aus dem Generator extrahieren, führt Python Code in der Funktion aus, bis eine yieldAnweisung vorliegt , hält das Objekt an und liefert es aus. Wenn Sie ein anderes Objekt extrahieren, wird Python direkt nach dem yieldfortgesetzt und fortgesetzt, bis es ein anderes erreicht yield(häufig dasselbe, aber eine Iteration später). Dies wird fortgesetzt, bis die Funktion am Ende abläuft. An diesem Punkt gilt der Generator als erschöpft.
Matthias Fripp

30
"Diese Iterables sind praktisch ... aber Sie speichern alle Werte im Speicher und dies ist nicht immer das, was Sie wollen", ist entweder falsch oder verwirrend. Ein iterable gibt einen Iterator zurück, wenn iter () auf dem iterable aufgerufen wird, und ein Iterator muss seine Werte nicht immer im Speicher speichern, abhängig von der Implementierung der iter- Methode. Er kann bei Bedarf auch Werte in der Sequenz generieren.
picmate 15

Es wäre schön, dieser großartigen Antwort hinzuzufügen, warum es genauso ist, außer dass Sie es ()anstelle von verwendet haben[] , insbesondere was es ()ist (es kann Verwechslungen mit einem Tupel geben).
WoJ

Ich kann mich irren, aber ein Generator ist kein Iterator, ein "genannter Generator" ist ein Iterator.
Aderchox

2006

Abkürzung zum Verständnis yield

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

  1. Fügen Sie result = []am Anfang der Funktion eine Zeile ein .
  2. Ersetzen Sie jedes yield exprdurch result.append(expr).
  3. Fügen Sie return resultam unteren Rand der Funktion eine Zeile ein .
  4. Ja - keine yieldAussagen mehr ! Code lesen und herausfinden.
  5. Vergleichen Sie die Funktion mit der ursprünglichen Definition.

Dieser Trick gibt Ihnen vielleicht eine Vorstellung von der Logik hinter der Funktion, aber was passiert eigentlich damit? yield unterscheidet sich erheblich von dem, was beim listenbasierten Ansatz passiert. In vielen Fällen ist der Ertragsansatz viel speichereffizienter und auch schneller. In anderen Fällen werden Sie mit diesem Trick in einer Endlosschleife stecken bleiben, obwohl die ursprüngliche Funktion einwandfrei funktioniert. Lesen Sie weiter, um mehr zu erfahren ...

Verwechseln Sie nicht Ihre Iterables, Iteratoren und Generatoren

Erstens das Iteratorprotokoll - wenn Sie schreiben

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

Python führt die folgenden zwei Schritte aus:

  1. Ruft einen Iterator für ab mylist :

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

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

  2. Verwendet den Iterator, um Elemente zu durchlaufen:

    Rufen Sie die next()Methode für den aus Schritt 1 zurückgegebenen Iterator weiter auf. Der Rückgabewert von next()wird zugewiesen xund der Schleifenkörper wird ausgeführt. Wenn eine Ausnahme StopIterationvon innen ausgelöst wird next(), bedeutet dies, dass der Iterator keine Werte mehr enthält und die Schleife beendet wird.

Die Wahrheit ist , Python , die über zwei Stufen zu jeder Zeit führt es will Schleife über den Inhalt eines Objekts - so ist es eine for - Schleife sein könnte, aber es könnte auch Code sein wie otherlist.extend(mylist)(wo otherlistist ein Python - Liste).

Hier mylistist eine iterable, weil es 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, beide __iter__()und next()dieselbe Klasse zu implementieren und eine __iter__()Rückgabe zu erhalten self. Dies funktioniert in einfachen Fällen, jedoch nicht, wenn zwei Iteratoren gleichzeitig dasselbe Objekt durchlaufen sollen.

Das ist also das Iteratorprotokoll. Viele Objekte implementieren dieses Protokoll:

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

Beachten Sie, dass eine forSchleife nicht weiß, um welche Art von Objekt es sich handelt - sie folgt nur dem Iteratorprotokoll und freut sich, beim Aufrufen Element für Element zu erhalten next(). Eingebaute Listen geben ihre Elemente einzeln zurück, Wörterbücher geben die Schlüssel einzeln zurück, Dateien geben die Zeilen einzeln zurück usw. Und Generatoren kehren zurück ... nun, hier yieldkommt Folgendes ins Spiel:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

yieldWenn Sie anstelle von Anweisungen drei returnAnweisungen f123()hätten, würde nur die erste ausgeführt und die Funktion beendet. Ist f123()aber keine gewöhnliche Funktion. Wenn f123()es aufgerufen wird, gibt es keinen der Werte in den Yield-Anweisungen zurück! Es gibt ein Generatorobjekt zurück. Außerdem wird die Funktion nicht wirklich beendet - sie wird angehalten. Wenn die forSchleife versucht, das Generatorobjekt zu durchlaufen, wird die Funktion in der nächsten Zeile nach der yieldvorherigen Rückgabe aus ihrem angehaltenen Zustand fortgesetzt, die nächste Codezeile, in diesem Fall eine yieldAnweisung, ausgeführt und als nächste zurückgegeben Artikel. Dies geschieht bis zum Beenden der Funktion. An diesem Punkt wird der Generator angehobenStopIteration wird und die Schleife beendet wird.

Das Generatorobjekt ähnelt also einem Adapter - an einem Ende zeigt es das Iteratorprotokoll, indem es __iter__()und next()Methoden verfügbar macht , um die forSchleife glücklich zu machen. Am anderen Ende wird die Funktion jedoch gerade so weit ausgeführt, dass der nächste Wert daraus hervorgeht, und sie wird wieder in den angehaltenen Modus versetzt.

Warum Generatoren verwenden?

Normalerweise können Sie Code schreiben, der keine Generatoren verwendet, aber dieselbe Logik implementiert. Eine Möglichkeit besteht darin, den zuvor erwähnten temporären Listentrick zu verwenden. Dies funktioniert nicht in allen Fällen, z. B. wenn Sie Endlosschleifen haben, oder wenn Sie eine wirklich lange Liste haben, wird der Speicher möglicherweise ineffizient genutzt. Der andere Ansatz besteht darin, eine neue iterierbare Klasse SomethingIter zu implementieren, die den Status in Instanzmitgliedern beibehält und den nächsten logischen Schritt in ihrer next()(oder __next__()in Python 3) Methode ausführt . Abhängig von der Logik next()sieht der Code in der Methode möglicherweise sehr komplex aus und ist anfällig für Fehler. Hier bieten Generatoren eine saubere und einfache Lösung.


20
"Wenn Sie eine Funktion mit Ertragsanweisungen sehen, wenden Sie diesen einfachen Trick an, um zu verstehen, was passieren wird." Ignoriert dies nicht die Tatsache, dass Sie sendin einen Generator einsteigen können , der einen großen Teil des Punktes von Generatoren ausmacht?
DanielSank

10
"Es könnte eine for-Schleife sein, aber es könnte auch Code wie otherlist.extend(mylist)" -> Dies ist falsch. extend()Ändert die Liste an Ort und Stelle und gibt keine iterable zurück. Der Versuch, eine Schleife otherlist.extend(mylist)durchzuführen, schlägt mit a fehl, TypeErrorda extend()implizit zurückgegeben Nonewird und Sie keine Schleife durchführen können None.
Pedro

4
@pedro Du hast diesen Satz falsch verstanden. Dies bedeutet, dass Python die beiden genannten Schritte bei der Ausführung aktiviert mylist(nicht otherlistaktiviert) hat otherlist.extend(mylist).
heute

555

Denk darüber so:

Ein Iterator ist nur ein ausgefallener Begriff für ein Objekt mit einer next()Methode. Eine Funktion mit Ertrag ist also ungefähr so:

Originalfassung:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

Dies ist im Grunde das, was der Python-Interpreter mit dem obigen Code macht:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

Um mehr darüber zu erfahren, was hinter den Kulissen passiert, kann die forSchleife folgendermaßen umgeschrieben werden:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

Ist das sinnvoller oder verwirrt es Sie nur mehr? :) :)

Ich sollte anmerken , dass dies ist eine grobe Vereinfachung zu illustrativen Zwecken. :) :)


1
__getitem__könnte anstelle von definiert werden __iter__. Zum Beispiel : class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i), Es wird gedruckt: 0, 10, 20, ..., 90
jfs

17
Ich habe dieses Beispiel in Python 3.6 ausprobiert und wenn ich es erstelle iterator = some_function(), hat die Variable iteratorkeine Funktion next()mehr, sondern nur noch eine __next__()Funktion. Ich dachte, ich würde es erwähnen.
Peter

Wo ruft die von forIhnen geschriebene Schleifenimplementierung die __iter__Methode von auf iterator, die instanziierte Instanz von it?
Systematische

455

Das yieldSchlüsselwort reduziert sich auf zwei einfache Fakten:

  1. Wenn der Compiler das yieldSchlüsselwort irgendwo in einer Funktion erkennt , wird diese Funktion nicht mehr über die returnAnweisung zurückgegeben. Stattdessen wird sofort ein verzögertes Objekt "Ausstehende Liste" zurückgegeben, das als Generator bezeichnet wird
  2. Ein Generator ist iterierbar. Was ist eine iterable ? Es ist so etwas wie eine listoder setoder rangeoder eine Diktatansicht mit einem integrierten Protokoll zum Aufrufen jedes Elements in einer bestimmten Reihenfolge .

Kurz gesagt: Ein Generator ist eine faule, inkrementell anstehende Liste . Mit yieldAnweisungen können Sie mithilfe der Funktionsnotation die Listenwerte programmieren, die der Generator schrittweise ausspucken soll.

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 wir eine Funktion makeRange, die der von Python ähnelt range. Der Aufruf makeRange(n)RÜCKKEHR A GENERATOR:

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 den Generator zu zwingen, seine ausstehenden Werte sofort zurückzugeben, können Sie ihn übergeben list()(genau wie bei jedem iterierbaren Element):

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

Vergleich des Beispiels mit "nur eine Liste zurückgeben"

Das obige Beispiel kann als bloße Erstellung einer Liste betrachtet werden, an die Sie anhängen und die Sie zurückgeben:

# 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 großen Unterschied; siehe den letzten Abschnitt.


Wie Sie Generatoren verwenden könnten

Eine Iterierbarkeit ist der letzte Teil eines Listenverständnisses, und alle Generatoren sind iterierbar, daher werden sie häufig folgendermaßen verwendet:

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

Um ein besseres Gefühl für Generatoren zu bekommen, können Sie mit dem itertoolsModul herumspielen (verwenden Sie es chain.from_iterablenicht, chainwenn dies gerechtfertigt ist). Zum Beispiel könnten Sie sogar Generatoren verwenden, um unendlich lange Lazy-Listen wie zu implementieren itertools.count(). Sie können Ihre eigene implementieren def enumerate(iterable): zip(count(), iterable)oder dies alternativ mit dem yieldSchlüsselwort in einer while-Schleife tun .

Bitte beachten Sie: Generatoren können tatsächlich für viele weitere Dinge verwendet werden, z. B. für die Implementierung von Coroutinen oder nicht deterministischer Programmierung oder andere elegante Dinge. Der hier vorgestellte Standpunkt "Lazy Lists" ist jedoch die häufigste Verwendung, die Sie finden werden.


Hinter den Kulissen

So funktioniert das "Python-Iterationsprotokoll". Das heißt, was ist los, wenn Sie es tun list(makeRange(5)). Dies ist, was ich früher als "faule, inkrementelle Liste" beschrieben habe.

>>> 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 nur die Objektfunktion auf .next(), die Teil des "Iterationsprotokolls" ist und auf allen Iteratoren zu finden 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 also, dies zu vermeiden ...


Minutien

Normalerweise interessieren sich die meisten Menschen nicht für die folgenden Unterscheidungen und möchten wahrscheinlich hier aufhören zu lesen.

In Python-speak ist eine Iterable jedes Objekt, das "das Konzept einer for-Schleife versteht" 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 der gleiche wie jeder Iterator, außer 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.

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 eine einmalige Verwendung. Wenn Sie es wiederverwenden möchten, sollten Sie myRange(...)erneut anrufen . 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. die furchtbar hackige Metaprogrammierung durchführen), können diese itertools.teebei Bedarf verwenden, da der Vorschlag für kopierbare Python- PEP- Standards für Iteratoren zurückgestellt wurde.


377

Was macht das yieldSchlüsselwort in Python?

Antwort Gliederung / Zusammenfassung

  • Eine Funktion mit yieldgibt beim Aufruf einen Generator zurück .
  • Generatoren sind Iteratoren, da sie das Iteratorprotokoll implementieren , sodass Sie über sie iterieren können.
  • Ein Generator kann auch Informationen senden , was ihn konzeptionell zu einer Coroutine macht .
  • In Python 3 können Sie mit in beide Richtungen von einem Generator an einen anderen delegierenyield from .
  • (Anhang kritisiert einige Antworten, einschließlich der obersten, und erläutert die Verwendung returnin einem Generator.)

Generatoren:

yieldist nur innerhalb einer Funktionsdefinition zulässig, und durch die Aufnahme yieldin eine Funktionsdefinition wird ein Generator zurückgegeben.

Die Idee für Generatoren stammt aus anderen Sprachen (siehe Fußnote 1) mit unterschiedlichen Implementierungen. In Pythons Generatoren ist die Ausführung des Codes eingefroren zum Zeitpunkt der Ausbeute . Wenn der Generator aufgerufen wird (Methoden werden unten erläutert), wird die Ausführung fortgesetzt und friert bei der nächsten Ausbeute ein.

yieldbietet eine einfache Möglichkeit zur Implementierung des Iteratorprotokolls , das durch die folgenden zwei Methoden definiert wird: __iter__und next(Python 2) oder __next__(Python 3). Beide Methoden machen ein Objekt zu einem Iterator, den Sie mit der IteratorAbstract Base Class aus dem collectionsModul überprüfen können .

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

Der Generatortyp ist ein Untertyp des Iterators:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

Und wenn nötig, können wir das wie folgt überprüfen:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Ein Merkmal von a Iterator ist, dass Sie es, sobald es erschöpft ist , nicht wiederverwenden oder zurücksetzen können:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

Sie müssen eine andere erstellen, wenn Sie die Funktionalität erneut verwenden möchten (siehe Fußnote 2):

>>> list(func())
['I am', 'a generator!']

Man kann Daten programmgesteuert liefern, zum Beispiel:

def func(an_iterable):
    for item in an_iterable:
        yield item

Der obige einfache Generator entspricht auch dem folgenden - ab Python 3.3 (und nicht in Python 2 verfügbar) können Sie Folgendes verwenden yield from:

def func(an_iterable):
    yield from an_iterable

yield fromErmöglicht jedoch auch die Delegierung an Subgeneratoren, was im folgenden Abschnitt über die kooperative Delegation mit Subkoroutinen erläutert wird.

Coroutinen:

yield bildet einen Ausdruck, mit dem Daten an den Generator gesendet werden können (siehe Fußnote 3)

Beachten Sie in diesem Beispiel die receivedVariable, die auf die Daten verweist, die an den Generator gesendet werden:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

Zuerst müssen wir den Generator mit der eingebauten Funktion in die Warteschlange stellen next. Abhängig von der verwendeten Python-Version wird die entsprechende Methode nextoder __next__Methode aufgerufen :

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

Und jetzt können wir Daten in den Generator senden. ( Senden Noneist dasselbe wie Anrufennext .):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

Kooperative Delegation bei Sub-Coroutine mit yield from

yield fromDenken Sie jetzt daran, dass dies in Python 3 verfügbar ist. Auf diese Weise können wir Coroutinen an eine Subkoroutine delegieren:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

Und jetzt können wir die Funktionalität an einen Untergenerator delegieren und sie kann wie oben von einem Generator verwendet werden:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

Weitere Informationen zur genauen Semantik finden Sie yield fromin PEP 380.

Andere Methoden: schließen und werfen

Die closeMethode wird GeneratorExitan dem Punkt ausgelöst, an dem die Funktionsausführung eingefroren wurde. Dies wird auch von aufgerufen, __del__damit Sie jeden Bereinigungscode dort ablegen können, wo Sie Folgendes ausführen GeneratorExit:

>>> my_account.close()

Sie können auch eine Ausnahme auslösen, die im Generator behandelt oder an den Benutzer zurückgegeben werden kann:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

Fazit

Ich glaube, ich habe alle Aspekte der folgenden Frage behandelt:

Was macht das yieldSchlüsselwort in Python?

Es stellt sich heraus, dass yielddas viel bewirkt. Ich bin sicher, ich könnte dem noch gründlichere Beispiele hinzufügen. Wenn Sie mehr wollen oder konstruktive Kritik haben, lassen Sie es mich wissen, indem Sie unten einen Kommentar abgeben.


Blinddarm:

Kritik der Top / Akzeptierte Antwort **

  • Es ist verwirrt darüber, was eine Iteration macht , nur anhand einer Liste als Beispiel. Siehe meine Referenzen oben, aber zusammenfassend: Eine Iterable hat eine __iter__Methode, die einen Iterator zurückgibt . Ein Iterator stellt eine .next(Python 2- oder .__next__(Python 3) -Methode bereit , die implizit von forSchleifen aufgerufen wird , bis sie ausgelöst wirdStopIteration wird. Sobald dies der Fall ist, wird dies auch weiterhin der Fall sein.
  • Anschließend wird mithilfe eines Generatorausdrucks beschrieben, was ein Generator ist. Da ein Generator einfach eine bequeme Möglichkeit ist, einen Iterator zu erstellen , verwirrt er die Sache nur, und wir sind noch nicht zu dem yieldTeil gekommen.
  • Bei der Steuerung einer Generatorerschöpfung ruft er die .nextMethode auf, wenn er stattdessen die eingebaute Funktion verwenden soll next. Dies wäre eine geeignete Indirektionsebene, da sein Code in Python 3 nicht funktioniert.
  • Itertools? Dies war überhaupt nicht relevant für das, was yieldtut.
  • Keine Diskussion der Methoden, yielddie zusammen mit der neuen Funktionalität yield fromin Python 3 bereitgestellt werden. Die oberste / akzeptierte Antwort ist eine sehr unvollständige Antwort.

Antwortkritik, die yieldin einem Generatorausdruck oder -verständnis vorschlägt .

Die Grammatik erlaubt derzeit jeden Ausdruck in einem Listenverständnis.

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

Da Ausbeute ein Ausdruck ist, wurde es von einigen als interessant angepriesen, ihn im Verständnis oder im Generatorausdruck zu verwenden - obwohl kein besonders guter Anwendungsfall angeführt wurde.

Die CPython-Kernentwickler diskutieren, die Zulässigkeit zu verwerfen . Hier ist ein relevanter Beitrag aus der Mailingliste:

Am 30. Januar 2017 um 19:05 schrieb Brett Cannon:

Am Sonntag, den 29. Januar 2017 um 16:39 Uhr schrieb Craig Rodrigues:

Ich bin mit beiden Ansätzen einverstanden. Es ist meiner Meinung nach nicht gut, die Dinge so zu belassen, wie sie in Python 3 sind.

Meiner Meinung nach handelt es sich um einen SyntaxError, da Sie nicht das bekommen, was Sie von der Syntax erwarten.

Ich würde zustimmen, dass dies ein vernünftiger Ort für uns ist, da jeder Code, der sich auf das aktuelle Verhalten stützt, wirklich zu clever ist, um gewartet werden zu können.

Um dorthin zu gelangen, werden wir wahrscheinlich Folgendes wollen:

  • SyntaxWarning oder DeprecationWarning in 3.7
  • Py3k-Warnung in 2.7.x.
  • SyntaxError in 3.8

Prost, Nick.

- Nick Coghlan | ncoghlan bei gmail.com | Brisbane, Australien

Darüber hinaus gibt es ein offenes Problem (10544), das darauf hinzudeuten scheint, dass dies niemals eine gute Idee ist (PyPy, eine in Python geschriebene Python-Implementierung, gibt bereits Syntaxwarnungen aus.)

Fazit: Bis die Entwickler von CPython uns etwas anderes sagen: Geben Sie keinen yieldgeneratorischen Ausdruck oder Verständnis ein.

Die returnAussage in einem Generator

In Python 2 :

In einer Generatorfunktion returndarf die Anweisung keine enthalten expression_list. In diesem Zusammenhang zeigt ein Bare returnan, dass der Generator fertig ist und StopIterationausgelöst wird.

An expression_listist im Grunde eine beliebige Anzahl von Ausdrücken, die durch Kommas getrennt sind. Im Wesentlichen können Sie in Python 2 den Generator stoppen return, aber keinen Wert zurückgeben.

In Python 3 :

In einer Generatorfunktion gibt die returnAnweisung an, dass der Generator fertig ist und StopIterationausgelöst wird. Der zurückgegebene Wert (falls vorhanden) wird als Argument zum Konstruieren verwendet StopIterationund wird zum StopIteration.valueAttribut.

Fußnoten

  1. In dem Vorschlag, das Konzept der Generatoren in Python einzuführen, wurde auf die Sprachen CLU, Sather und Icon verwiesen. Die allgemeine Idee ist, dass eine Funktion den internen Zustand beibehalten und auf Anforderung des Benutzers Zwischendatenpunkte liefern kann. Dies versprach eine überlegene Leistung gegenüber anderen Ansätzen, einschließlich Python-Threading , das auf einigen Systemen nicht einmal verfügbar ist.

  2. Dies bedeutet beispielsweise, dass xrangeObjekte ( rangein Python 3) keine Iterators sind, obwohl sie iterierbar sind, da sie wiederverwendet werden können. Wie Listen geben ihre __iter__Methoden Iteratorobjekte zurück.

  3. yieldwurde ursprünglich als Anweisung eingeführt, was bedeutet, dass sie nur am Anfang einer Zeile in einem Codeblock erscheinen konnte. Erstellt jetzt yieldeinen Ertragsausdruck. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Diese Änderung wurde vorgeschlagen , damit ein Benutzer Daten so an den Generator senden kann, wie er sie möglicherweise erhält. Um Daten zu senden, muss man sie etwas zuweisen können, und dafür funktioniert eine Anweisung einfach nicht.


328

yieldist wie return- es gibt alles zurück, was Sie ihm sagen (als Generator). Der Unterschied besteht darin, dass beim nächsten Aufruf des Generators die Ausführung vom letzten Aufruf der yieldAnweisung beginnt . Im Gegensatz zur Rückgabe wird der Stapelrahmen nicht bereinigt, wenn eine Ausbeute auftritt. Die Steuerung wird jedoch zurück an den Aufrufer übertragen, sodass sein Status beim nächsten Aufruf der Funktion fortgesetzt wird.

Im Fall Ihres Codes get_child_candidatesverhält sich die Funktion wie ein Iterator, sodass beim Erweitern Ihrer Liste jeweils ein Element zur neuen Liste hinzugefügt wird.

list.extendruft einen Iterator auf, bis er erschöpft ist. Im Fall des von Ihnen veröffentlichten Codebeispiels wäre es viel klarer, nur ein Tupel zurückzugeben und dieses an die Liste anzuhängen.


107
Dies ist nah, aber nicht korrekt. Jedes Mal, wenn Sie eine Funktion mit einer Yield-Anweisung aufrufen, wird ein brandneues Generatorobjekt zurückgegeben. Nur wenn Sie die .next () -Methode dieses Generators aufrufen, wird die Ausführung nach dem letzten Ertrag fortgesetzt.
Kurosch

239

Es gibt noch eine weitere Sache zu erwähnen: Eine Funktion, die ergibt, muss nicht wirklich beendet werden. Ich habe folgenden Code geschrieben:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

Dann kann ich es in einem anderen Code wie diesem verwenden:

for f in fib():
    if some_condition: break
    coolfuncs(f);

Es hilft wirklich, einige Probleme zu vereinfachen und erleichtert die Arbeit mit einigen Dingen.


233

Wenn Sie 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

209

TL; DR

An Stelle von:

def square_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 square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

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

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


yieldist eine zuckerhaltige Art zu sagen

baue eine Reihe von Sachen

Gleiches Verhalten:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

Anderes Verhalten:

Die Ausbeute erfolgt in einem Durchgang : Sie können nur einmal durchlaufen. Wenn eine Funktion eine Ausbeute enthält, nennen wir sie eine Generatorfunktion . Und ein Iterator ist das, was er zurückgibt. Diese Begriffe sind aufschlussreich. Wir verlieren die Bequemlichkeit eines Containers, gewinnen aber die Leistung einer Serie, die nach Bedarf berechnet wird und beliebig lang ist.

Die Ausbeute ist faul , sie verschiebt die Berechnung. Eine Funktion mit einem Ertrag wird beim Aufrufen überhaupt nicht ausgeführt. Es gibt ein Iteratorobjekt zurück , das sich merkt, wo es aufgehört hat. Jedes Mal, wenn Sie next()den Iterator aufrufen (dies geschieht in einer for-Schleife), wird die Ausführung nur wenige Zentimeter vor dem nächsten Ertrag ausgeführt. returnlöst StopIteration aus und beendet die Serie (dies ist das natürliche Ende einer for-Schleife).

Der Ertrag ist vielseitig . Daten müssen nicht alle zusammen gespeichert werden, sondern können einzeln zur Verfügung gestellt werden. Es kann unendlich sein.

>>> 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(square_yield(4))
[0, 1, 4, 9]

Geniale Wortwahl, yieldda beide Bedeutungen zutreffen:

Ertrag - produzieren oder liefern (wie in der Landwirtschaft)

... liefern die nächsten Daten in der Reihe.

Nachgeben - nachgeben oder aufgeben (wie in der politischen Macht)

... die CPU-Ausführung aufgeben, bis der Iterator voranschreitet.


194

Ausbeute gibt Ihnen einen Generator.

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

Wie Sie sehen können, wird im ersten Fall foodie gesamte Liste auf einmal gespeichert. Es ist keine große Sache für eine Liste mit 5 Elementen, aber was ist, wenn Sie eine Liste mit 5 Millionen wollen? Dies ist nicht nur ein großer Speicherfresser, es kostet auch viel Zeit, um es zum Zeitpunkt des Aufrufs der Funktion zu erstellen.

Im zweiten Fall erhalten barSie nur einen Generator. Ein Generator ist iterierbar - das heißt, Sie können ihn in einer forSchleife usw. verwenden, aber auf jeden Wert kann nur einmal zugegriffen werden. Es werden auch nicht alle Werte gleichzeitig gespeichert. Das Generatorobjekt "merkt" sich, wo es sich beim letzten Aufruf in der Schleife befand. Wenn Sie also eine Iterable verwenden, um beispielsweise 50 Milliarden zu zählen, müssen Sie nicht alle bis 50 Milliarden zählen sofort und speichern Sie die 50 Milliarden Zahlen, um durchzuzählen.

Auch dies ist ein ziemlich ausgeklügeltes Beispiel. Sie würden wahrscheinlich itertools verwenden, wenn Sie wirklich bis 50 Milliarden zählen wollten. :) :)

Dies ist der einfachste Anwendungsfall von Generatoren. Wie Sie sagten, kann es verwendet werden, um effiziente Permutationen zu schreiben, wobei Yield verwendet wird, um Dinge durch den Aufrufstapel zu schieben, anstatt eine Art Stapelvariable zu verwenden. Generatoren können auch für die spezielle Baumdurchquerung und für alle möglichen anderen Zwecke verwendet werden.


Nur eine Anmerkung - in Python 3 wird rangeanstelle einer Liste auch ein Generator zurückgegeben, sodass Sie auch eine ähnliche Idee sehen, außer dass __repr__/ __str__überschrieben werden, um in diesem Fall ein besseres Ergebnis zu erzielen range(1, 10, 2).
wahr.

189

Es gibt einen Generator zurück. Ich bin mit Python nicht besonders vertraut, aber ich glaube, es ist das Gleiche wie die Iteratorblöcke von C #, wenn Sie mit diesen vertraut sind.

Die Schlüsselidee ist, dass der Compiler / Interpreter / was auch immer einige Tricks macht, damit der Aufrufer weiterhin next () aufrufen kann und weiterhin Werte zurückgibt - als ob die Generatormethode angehalten worden wäre . Offensichtlich können Sie eine Methode nicht wirklich "pausieren", daher erstellt der Compiler eine Zustandsmaschine, damit Sie sich daran erinnern können, wo Sie sich gerade befinden und wie die lokalen Variablen usw. aussehen. Dies ist viel einfacher als selbst einen Iterator zu schreiben.


167

Es gibt eine Art von Antwort, von der ich glaube, dass sie noch nicht gegeben wurde, unter den vielen großartigen Antworten, die den Umgang mit Generatoren beschreiben. Hier ist die Antwort auf die Programmiersprachtheorie:

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, aber Fortsetzungen stellen den allgemeineren Mechanismus dar, um zu verstehen, was vor sich geht).

Fortsetzungen in der Theorie der Programmiersprachen sind eine viel grundlegendere Art der Berechnung, werden jedoch nicht oft verwendet, da sie äußerst schwer zu überlegen und auch sehr schwer zu implementieren sind. Die Vorstellung, was eine Fortsetzung ist, ist jedoch unkompliziert: Es ist der Zustand einer Berechnung, der noch nicht abgeschlossen ist. In diesem Zustand werden die aktuellen Werte von Variablen, die noch auszuführenden Operationen usw. gespeichert. Dann kann zu einem späteren Zeitpunkt im Programm die Fortsetzung aufgerufen werden, so dass die Variablen des Programms 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. Auf diese call/ccWeise wird der Stapel des Programms buchstäblich gespeichert, und wenn die Fortsetzung aufgerufen wird, wird der Stapel wiederhergestellt.

Im Continuation Passing Style (CPS) sind Fortsetzungen nur 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 eher durch Verschlüsse (und die darin codierten Variablen) als durch Variablen dargestellt, die sich irgendwo auf dem Stapel befinden. Funktionen, die den Kontrollfluss verwalten, akzeptieren die Fortsetzung als Argumente (in einigen Variationen von CPS akzeptieren Funktionen möglicherweise mehrere Fortsetzungen) und manipulieren den Kontrollfluss, indem sie sie aufrufen, indem sie einfach aufgerufen und anschließend zurückgegeben werden. Ein sehr einfaches Beispiel für den Continuation-Passing-Stil lautet wie folgt:

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 vereinfachten) Beispiel speichert der Programmierer die Operation des tatsächlichen Schreibens der Datei in eine Fortsetzung (was möglicherweise eine sehr komplexe Operation mit vielen zu schreibenden Details sein kann) und übergibt diese Fortsetzung dann (dh als erste). Klassenschließung) an einen anderen Operator, der weitere Verarbeitungen vornimmt und diese dann bei Bedarf aufruft. (Ich verwende dieses Entwurfsmuster häufig in der eigentlichen GUI-Programmierung, entweder weil es mir Codezeilen spart oder, was noch wichtiger ist, um den Kontrollfluss nach dem Auslösen von GUI-Ereignissen zu verwalten.)

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


Lassen Sie uns nun über Generatoren in Python sprechen. Generatoren sind ein spezifischer Subtyp der Fortsetzung. Während Fortsetzungen im Allgemeinen den Status einer Berechnung (dh den Aufrufstapel des Programms) speichern können , können Generatoren den Iterationsstatus nur über einen Iterator speichern . Diese Definition ist jedoch für bestimmte Anwendungsfälle von Generatoren leicht irreführend. Zum Beispiel:

def f():
  while True:
    yield 4

Dies ist eindeutig eine vernünftige Iterierbarkeit, deren Verhalten genau definiert ist. Jedes Mal, wenn der Generator darüber iteriert, gibt er 4 zurück (und dies für immer). Aber es ist wahrscheinlich nicht der prototypische Typ von Iterable, der in den Sinn kommt, wenn man an Iteratoren denkt (dh for x in collection: do_something(x)). Dieses Beispiel zeigt die Leistung von Generatoren: Wenn etwas ein Iterator ist, kann ein Generator den Status seiner Iteration speichern.

Um es noch einmal zu wiederholen: Fortsetzungen können den Status des Stacks eines Programms speichern, und Generatoren können den Status der Iteration speichern. Dies bedeutet, dass Fortsetzungen viel leistungsfähiger sind als Generatoren, aber auch, dass Generatoren viel, viel einfacher sind. Sie sind für den Sprachdesigner einfacher zu implementieren und für den Programmierer einfacher zu verwenden (wenn Sie etwas Zeit zum Brennen haben, versuchen Sie, diese Seite über Fortsetzungen und Aufruf / cc zu lesen und zu verstehen ).

Sie können Generatoren jedoch leicht als einfachen, spezifischen Fall eines Continuation-Passing-Stils implementieren (und konzipieren):

Bei jedem yieldAufruf wird die Funktion angewiesen, 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 im Wesentlichen 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 tatsächlich syntaktischer Zucker für die eigentliche 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. Versuchen Sie jedoch, um zu verstehen, was vor sich geht, den Continuation-Passing-Stil zu verwenden, um Generatorobjekte ohne Verwendung des yieldSchlüsselworts zu implementieren .


152

Hier ist ein Beispiel im Klartext. Ich werde eine Entsprechung zwischen menschlichen Konzepten auf hoher Ebene und Python-Konzepten auf niedriger Ebene bereitstellen.

Ich möchte eine Folge von Zahlen bearbeiten, aber ich möchte mich nicht mit der Erstellung dieser Folge beschäftigen, sondern mich nur auf die Operation konzentrieren, die ich ausführen möchte. Also mache ich folgendes:

  • Ich rufe Sie an und sage Ihnen, dass ich eine Folge von Zahlen möchte, die auf eine bestimmte Weise erzeugt wird, und ich lasse Sie wissen, was der Algorithmus ist.
    Dieser Schritt entspricht der defEingabe der Generatorfunktion, dh der Funktion, die a enthält yield.
  • Einige Zeit später sage ich Ihnen: "OK, machen Sie sich bereit, mir die Reihenfolge der Zahlen mitzuteilen."
    Dieser Schritt entspricht dem Aufruf der Generatorfunktion, die ein Generatorobjekt zurückgibt. Beachten Sie, dass Sie mir noch keine Zahlen mitteilen. Sie greifen einfach nach Papier und Bleistift.
  • Ich frage dich, "sag mir die nächste Nummer", und du sagst mir die erste Nummer; Danach warten Sie darauf, dass ich Sie nach der nächsten Nummer frage. Es ist Ihre Aufgabe, sich daran zu erinnern, wo Sie waren, welche Nummern Sie bereits gesagt haben und was die nächste Nummer ist. Die Details sind mir egal.
    Dieser Schritt entspricht dem Aufruf .next()des Generatorobjekts.
  • … Wiederholen Sie den vorherigen Schritt, bis…
  • Schließlich könnten Sie zu einem Ende kommen. Du sagst mir keine Nummer; Sie schreien nur: "Halten Sie Ihre Pferde! Ich bin fertig! Keine Zahlen mehr!"
    Dieser Schritt entspricht dem Generatorobjekt, das seinen Job beendet und eine StopIterationAusnahme auslöst. Die Generatorfunktion muss die Ausnahme nicht auslösen. Es wird automatisch ausgelöst, wenn die Funktion endet oder a ausgibt return.

Dies ist, was ein Generator tut (eine Funktion, die a enthält yield); Es wird ausgeführt, pausiert, wann immer es ausgeführt wird yield, und wenn es nach einem .next()Wert gefragt wird, wird es an dem Punkt fortgesetzt, an dem es zuletzt war. Es passt vom Design her perfekt zum Iterator-Protokoll von Python, das beschreibt, wie Werte nacheinander angefordert werden.

Der bekannteste Benutzer des Iterator-Protokolls ist der forBefehl in Python. Also, wann immer Sie eine:

for item in sequence:

Es spielt keine Rolle, ob sequencees sich um eine Liste, eine Zeichenfolge, ein Wörterbuch oder ein Generatorobjekt handelt , wie oben beschrieben. Das Ergebnis ist das gleiche: Sie lesen Elemente einzeln aus einer Sequenz.

Beachten Sie, dass das defEingeben einer Funktion, die ein yieldSchlüsselwort enthält, nicht die einzige Möglichkeit ist, einen Generator zu erstellen. Es ist nur der einfachste Weg, einen zu erstellen.

Weitere Informationen zu Iteratortypen , der Yield-Anweisung und den Generatoren finden Sie in der Python-Dokumentation.


130

Während viele Antworten zeigen, warum Sie einen verwenden würden yield, um einen Generator zu erstellen, gibt es mehr Verwendungsmö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 guten Beispiele wiederholen, die bereits yieldzum Erstellen eines Generators verwendet wurden.

Um zu verstehen, was a yieldim folgenden Code tut, können Sie den Zyklus mit Ihrem Finger durch jeden Code mit a verfolgen yield. Jedes Mal, wenn Ihr Finger auf den Finger trifft yield, müssen Sie warten, bis ein nextoder ein sendeingegeben wird. 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 nextes erneut aufgerufen wird, führen Sie eine weitere Schleife durch den Code durch. Sie werden jedoch feststellen, dass in einer Coroutine yieldauch mit einem send… verwendet werden kann, das einen Wert vom Aufrufer an die Yielding-Funktion sendet . Wenn einsend gegeben ist, dannyieldEmpfängt den gesendeten Wert und spuckt ihn auf der linken Seite aus. Dann wird die Verfolgung durch den Code fortgesetzt, bis Sie aufgerufen wurde).yield wieder (Rückgabe des Wertes am Ende, als ob next

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()

Niedlich! Ein Trampolin (im Lisp-Sinne). Nicht oft sieht man die!
00prometheus

129

Es gibt eine andere yieldVerwendung und Bedeutung (seit Python 3.3):

yield from <expr>

Aus PEP 380 - Syntax zum Delegieren an einen Subgenerator :

Für einen Generator wird eine Syntax vorgeschlagen, um einen Teil seiner Operationen an einen anderen Generator zu delegieren. Auf diese Weise kann ein Codeabschnitt, der 'Ertrag' enthält, herausgerechnet und in einem anderen Generator abgelegt werden. Außerdem darf der Untergenerator mit einem Wert zurückkehren, und der Wert wird dem delegierenden Generator zur Verfügung gestellt.

Die neue Syntax eröffnet auch einige Optimierungsmöglichkeiten, wenn ein Generator die von einem anderen erzeugten Werte wiedergibt.

Darüber hinaus wird dies (seit Python 3.5) einführen:

async def new_coroutine(data):
   ...
   await blocking_action()

um zu vermeiden, dass Coroutinen mit einem normalen Generator verwechselt werden (wird heute yieldin beiden verwendet).


117

Alles gute Antworten, allerdings etwas schwierig für Neulinge.

Ich gehe davon aus, dass Sie die returnAussage gelernt haben .

Als Analogie returnund yieldsind Zwillinge. returnbedeutet "Rückkehr und Stopp", während "Rendite" "Rückkehr, aber Fortsetzung" bedeutet.

  1. Versuchen Sie eine num_list mit zu bekommen return.
def num_list(n):
    for i in range(n):
        return i

Starte es:

In [5]: num_list(3)
Out[5]: 0

Sie erhalten nur eine einzige Nummer und keine Liste. returnErlaubt dir niemals, dich glücklich durchzusetzen, implementiert nur einmal und beendet.

  1. Da kommt yield

Ersetzen returndurch yield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

Jetzt gewinnen Sie, um alle Zahlen zu erhalten.

Wenn Sie vergleichen, returnwelche einmal ausgeführt und angehalten werden, werden yielddie von Ihnen geplanten Zeiten ausgeführt. Sie können returnals return one of themund yieldals interpretieren return all of them. Dies nennt man iterable.

  1. Ein weiterer Schritt, mit dem wir die yieldAnweisung umschreiben könnenreturn
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

Es geht um den Kern yield.

Der Unterschied zwischen einer Listenausgabe returnund der Objektausgabe yieldist:

Sie erhalten immer [0, 1, 2] von einem Listenobjekt, können diese jedoch nur einmal aus der 'Objektausgabe yield' abrufen . Es hat also ein neues generatorNamensobjekt, wie in angezeigt Out[11]: <generator object num_list at 0x10327c990>.

Abschließend als Metapher, um es zu verstehen:

  • returnund yieldsind Zwillinge
  • listund generatorsind Zwillinge

Dies ist verständlich, aber ein Hauptunterschied besteht darin, dass Sie in einer Funktion / Methode mehrere Ausbeuten erzielen können. Die Analogie bricht an diesem Punkt völlig zusammen. Yield merkt sich seinen Platz in einer Funktion. Wenn Sie also das nächste Mal next () aufrufen, wird Ihre Funktion mit der nächsten fortgesetzt yield. Dies ist wichtig, denke ich, und sollte zum Ausdruck gebracht werden.
Mike S

104

Hier sind einige Python-Beispiele, wie Generatoren tatsächlich implementiert werden, als ob Python keinen syntaktischen Zucker für sie bereitstellen würde:

Als Python-Generator:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

Verwendung von lexikalischen Verschlüssen anstelle von Generatoren

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

Verwenden von Objektschließungen anstelle von Generatoren (weil ClosuresAndObjectsAreEquivalent )

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

97

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

Beachten Sie auch, dass yieldin Coroutinen die doppelte Verwendung in Generatorfunktionen verwendet werden kann. Obwohl es nicht die gleiche Verwendung wie Ihr Code-Snippet ist, (yield)kann es als Ausdruck in einer Funktion verwendet werden. Wenn ein Aufrufer mithilfe 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 Möglichkeit, Anwendungen vom Typ Datenfluss einzurichten. Ich dachte, es lohnt sich, über die andere Verwendung der yieldAnweisung in Funktionen Bescheid zu wissen .


97

Aus programmtechnischer Sicht sind 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, werden Nachrichten verwendet, die an ein Abschlussobjekt gesendet werden, das einen Dispatcher hat, und der Dispatcher antwortet auf "Nachrichten".

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

" next " ist eine Nachricht, die an einen Abschluss gesendet wird und vom " iter " erstellt wird " -Aufruf erstellt wurde.

Es gibt viele Möglichkeiten, diese Berechnung zu implementieren. Ich habe eine Mutation verwendet, aber es ist einfach, dies ohne Mutation zu tun, indem der aktuelle Wert und der nächste Ertrag 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
->

84

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 sieht für mich so aus yield ob die Position des Programmflusses und die nächste Schleife von der Position "Yield" ausgehen. Es scheint, als würde es an dieser Position warten und kurz davor einen Wert nach draußen zurückgeben und das nächste Mal weiterarbeiten.

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


Du hast Recht. Aber wie wirkt sich das auf den Durchfluss aus, um das Verhalten von "Ertrag" zu sehen? Ich kann den Algorithmus im Namen der Mathematik ändern. Wird es helfen, eine andere Bewertung des "Ertrags" zu erhalten?
Engin OZTURK

68

Hier ist ein mentales Bild von dem, was yieldtut.

Ich stelle mir einen Thread gerne als Stack vor (auch wenn er nicht so implementiert ist).

Wenn eine normale Funktion aufgerufen wird, legt sie ihre lokalen Variablen auf den Stapel, führt einige Berechnungen durch, löscht dann den Stapel und kehrt zurück. Die Werte der lokalen Variablen werden nie wieder angezeigt.

yieldWenn bei einer Funktion die Ausführung ihres Codes beginnt (dh nachdem die Funktion aufgerufen wurde und ein Generatorobjekt zurückgegeben wird, dessen next()Methode dann aufgerufen wird), legt sie ihre lokalen Variablen auf ähnliche Weise auf den Stapel und berechnet eine Weile. Wenn es jedoch auf die yieldAnweisung trifft , bevor es seinen Teil des Stapels löscht und zurückgibt, erstellt es eine Momentaufnahme seiner lokalen Variablen und speichert sie im Generatorobjekt. Es schreibt auch die Stelle, an der es sich gerade befindet, in seinen Code (dh die bestimmte yieldAnweisung).

Es ist also eine Art eingefrorene Funktion, an der der Generator festhält.

Wenn next()es später aufgerufen wird, ruft es die Habseligkeiten der Funktion auf dem Stapel ab und animiert sie erneut. Die Funktion berechnet weiterhin dort, wo sie aufgehört hat, ohne zu wissen, dass sie gerade eine Ewigkeit im Kühlhaus verbracht hat.

Vergleichen Sie die folgenden Beispiele:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

Wenn wir die zweite Funktion aufrufen, verhält sie sich ganz anders als die erste. Die yieldAussage ist möglicherweise nicht erreichbar, aber wenn sie irgendwo vorhanden ist, ändert sie die Art des Umgangs mit uns.

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

Beim Aufrufen yielderFunction()wird der Code nicht ausgeführt, sondern aus dem Code wird ein Generator erstellt. (Vielleicht ist es eine gute Idee, solche Dinge mit dem yielderPräfix für Lesbarkeit zu benennen .)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

Die gi_codeund gi_frameFelder sind , wo der gefrorene Zustand gelagert wird. Wenn dir(..)wir sie mit untersuchen , können wir bestätigen, dass unser oben genanntes mentales Modell glaubwürdig ist.


59

Wie jede Antwort andeutet, yieldwird zum Erstellen eines Sequenzgenerators verwendet. Es wird zum dynamischen Generieren einer Sequenz verwendet. Wenn Sie beispielsweise eine Datei zeilenweise in einem Netzwerk lesen, können Sie die folgende yieldFunktion 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)

Execution Control Transfer gotcha

Die Ausführungssteuerung wird von getNextLines () in die forSchleife übertragen, wenn Yield ausgeführt wird. Daher beginnt die Ausführung jedes Mal, wenn getNextLines () aufgerufen wird, an dem Punkt, an dem sie das letzte Mal angehalten wurde.

Kurz gesagt, eine Funktion mit dem folgenden 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 gedruckt

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

59

Ein einfaches Beispiel, um zu verstehen, was es ist: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

Die Ausgabe ist:

1 2 1 2 1 2 1 2

5
Bist du dir bei dieser Ausgabe sicher? Würde das nicht nur in einer einzigen Zeile gedruckt, wenn Sie diese Druckanweisung mit ausführen würden print(i, end=' ')? Andernfalls glaube ich, dass das Standardverhalten jede Zahl in eine neue Zeile setzen würde
user9074332

@ user9074332, Sie haben Recht, aber es steht in einer Zeile, um das Verständnis zu erleichtern
Gavriel Cohen

57

(Meine folgende Antwort spricht nur aus der Perspektive der Verwendung des Python-Generators, nicht aus der zugrunde liegenden Implementierung des Generatormechanismus bezieht sich , der einige Tricks der Stapel- und Heap-Manipulation beinhaltet.)

Wenn yieldanstelle einer returnin einer Python-Funktion verwendet wird, wird diese Funktion in etwas Besonderes umgewandelt, das als bezeichnet wird generator function. Diese Funktion gibt ein Objekt vom generatorTyp zurück. Das yieldSchlüsselwort ist ein Flag, mit dem der Python-Compiler benachrichtigt wird, diese Funktion speziell zu behandeln. Normale Funktionen werden beendet, sobald ein Wert zurückgegeben wird. Mit Hilfe des Compilers kann die Generatorfunktion jedoch als wiederaufnehmbar angesehen werden. Das heißt, der Ausführungskontext wird wiederhergestellt und die Ausführung wird vom letzten Lauf fortgesetzt. Bis Sie return explizit aufrufen, wodurch eine StopIterationAusnahme ausgelöst wird (die auch Teil des Iteratorprotokolls ist) oder das Ende der Funktion erreichen. Ich habe viele Referenzen übergenerator aber dies einvon der functional programming perspectiveist am verdaulichsten.

(Jetzt möchte ich über die Gründe sprechen, die dahinter stehen generator, und die iteratorauf meinem eigenen Verständnis beruhen. Ich hoffe, dies kann Ihnen helfen, die wesentliche Motivation von Iterator und Generator zu erfassen . Ein solches Konzept wird auch in anderen Sprachen wie C # angezeigt.)

Soweit ich weiß, speichern wir die Daten, wenn wir eine Reihe von Daten verarbeiten möchten, normalerweise zuerst irgendwo und verarbeiten sie dann einzeln. Dieser naive Ansatz ist jedoch problematisch. Wenn das Datenvolumen sehr groß ist, ist es teuer, es vorher als Ganzes zu speichern. Also, anstatt das sich dataselbst direkt zu speichern, warum nicht eine Art metadataindirekt speichern , dhthe logic how the data is computed .

Es gibt zwei Ansätze, um solche Metadaten zu verpacken.

  1. Beim OO-Ansatz verpacken wir die Metadaten as a class. Dies ist der sogenannte, iteratorder das Iteratorprotokoll implementiert (dh die __next__(), und __iter__()Methoden). Dies ist auch das häufig verwendete Iterator-Entwurfsmuster .
  2. Beim funktionalen Ansatz verpacken wir die Metadaten as a function. Dies ist das sogenannte generator function. Unter der Haube ist der zurückgegebene Iterator jedoch generator objectimmer noch vorhanden, IS-Ada er auch das Iteratorprotokoll implementiert.

In beiden Fällen 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, welche Sie verwenden, liegt bei Ihnen.


54

Zusammenfassend yieldwandelt die Anweisung Ihre Funktion in eine Factory um, die ein spezielles Objekt namens a erzeugt, generatordas den Körper Ihrer ursprünglichen Funktion umschließt. Wenn das generatoriteriert wird, führt es Ihre Funktion aus, bis es das nächste erreicht, setzt yielddann die Ausführung aus und wertet den übergebenen Wert aus yield. Dieser Vorgang wird bei jeder Iteration wiederholt, bis der Ausführungspfad die Funktion verlässt. Zum Beispiel,

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

for i in simple_generator():
    print i

einfach ausgibt

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 "liefern". Auf diese Weise berechnet er eine Liste im laufenden Betrieb, wobei der Vorteil der Speicher ist für besonders große Berechnungen gespeichert

Angenommen, Sie möchten eine eigene rangeFunktion erstellen , die einen iterierbaren Zahlenbereich erzeugt. Sie können dies folgendermaßen 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

Das ist aber ineffizient, weil

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

Glücklicherweise waren Guido und sein Team großzügig genug, um Generatoren zu entwickeln, damit 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 eine Funktion auf dem aufgerufenen Generator next()die Funktion aus, bis sie entweder eine 'Yield'-Anweisung erreicht, in der sie stoppt und den Wert' ergibt 'oder das Ende der Funktion erreicht. In diesem Fall wird beim ersten Aufruf next()die Yield-Anweisung und Yield 'n' ausgeführt. Beim nächsten Aufruf wird die Inkrement-Anweisung ausgeführt, zum 'while' zurückgesprungen, ausgewertet und bei true gestoppt und gestoppt Geben Sie 'n' wieder ein, so wird es fortgesetzt, bis die while-Bedingung false zurückgibt und der Generator zum Ende der Funktion springt.


53

Ertrag ist ein Objekt

EIN return in einer Funktion gibt einen einzelnen Wert zurück.

Wenn eine Funktion eine große Anzahl von Werten zurückgeben soll , verwenden Sieyield .

Noch wichtiger yieldist eine Barriere .

Wie Barriere in der CUDA-Sprache wird die Kontrolle erst übertragen, wenn sie abgeschlossen ist.

Das heißt, es wird der Code in Ihrer Funktion von Anfang an ausgeführt, bis er trifft yield . Dann wird der erste Wert der Schleife zurückgegeben.

Dann führt jeder zweite Aufruf die Schleife, die Sie in die Funktion geschrieben haben, noch einmal aus und gibt den nächsten Wert zurück, bis kein Wert mehr zurückzugeben ist.


52

Viele Menschen verwenden returneher als yield, aber in einigen Fällen yieldkann es effizienter und einfacher sein, damit zu arbeiten.

Hier ist ein Beispiel, das yielddefinitiv am besten für:

return (in Funktion)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

Ausbeute (in Funktion)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

Funktionen aufrufen

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

Beide Funktionen machen dasselbe, yieldverwenden jedoch drei statt fünf Zeilen und haben eine Variable weniger, über die Sie sich Sorgen machen müssen.

Dies ist das Ergebnis des Codes:

Ausgabe

Wie Sie sehen, machen beide Funktionen dasselbe. Der einzige Unterschied besteht darin, return_dates()eine Liste und yield_dates()einen Generator anzugeben.

Ein Beispiel aus dem wirklichen Leben wäre so etwas wie das zeilenweise Lesen einer Datei oder wenn Sie nur einen Generator erstellen möchten.


43

yieldist wie ein Rückgabeelement für eine Funktion. Der Unterschied besteht darin, dass das yieldElement eine Funktion in einen Generator verwandelt. Ein Generator verhält sich wie eine Funktion, bis etwas "nachgegeben" wird. Der Generator stoppt bis zum nächsten Aufruf und fährt genau an dem Punkt fort, an dem er gestartet wurde. Sie können eine Folge aller 'ergebenen' Werte in einem erhalten, indem Sie aufrufen list(generator()).


41

Das yieldSchlüsselwort sammelt einfach wiederkehrende Ergebnisse. Denken Sie an yieldwiereturn +=


36

Hier ist ein einfacher yieldAnsatz zur Berechnung der Fibonacci-Reihe, der erklärt wird:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

Wenn Sie dies in Ihre REPL eingeben und dann versuchen, es aufzurufen, erhalten Sie ein mysteriöses Ergebnis:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

Dies liegt daran yield, dass Python signalisiert, dass Sie einen Generator erstellen möchten, dh ein Objekt, das bei Bedarf Werte generiert.

Wie generieren Sie diese Werte? Dies kann entweder direkt über die integrierte Funktion erfolgennext oder indirekt durch Einspeisung in ein Konstrukt erfolgen, das Werte verbraucht.

Mit der integrierten next()Funktion rufen Sie .next/ direkt auf __next__und zwingen den Generator, einen Wert zu erzeugen:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

Indirekt "verbrauchen" Sie den Generator , wenn Sie fibeiner forSchleife, einem listInitialisierer, einem tupleInitialisierer oder etwas anderem, das ein Objekt erwartet, das Werte generiert / erzeugt, etwas bereitstellen , bis keine Werte mehr von ihm erzeugt werden können (und es zurückkehrt). ::

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

Ebenso mit einem tupleInitialisierer:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

Ein Generator unterscheidet sich von einer Funktion dadurch, dass er faul ist. Dies wird erreicht, indem der lokale Status beibehalten wird und Sie jederzeit fortfahren können.

Wenn Sie es zum ersten Mal aufrufen, fibindem Sie es aufrufen:

f = fib()

Python kompiliert die Funktion, trifft auf das yieldSchlüsselwort und gibt einfach ein Generatorobjekt an Sie zurück. Es scheint nicht sehr hilfreich zu sein.

Wenn Sie dann anfordern, dass es direkt oder indirekt den ersten Wert generiert, führt es alle gefundenen Anweisungen aus, bis es auf a trifft yield, und gibt dann den Wert zurück, den Sie angegeben haben, yieldund pausiert. Verwenden Sie für ein Beispiel, das dies besser demonstriert, einige printAufrufe (ersetzen Sie sie durch print "text"if unter Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

Geben Sie nun die REPL ein:

>>> gen = yielder("Hello, yield!")

Sie haben jetzt ein Generatorobjekt, das auf einen Befehl wartet, damit es einen Wert generiert. Verwenden Sie nextund sehen Sie, was gedruckt wird:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Die nicht zitierten Ergebnisse sind das, was gedruckt wird. Das angegebene Ergebnis ist das, von dem zurückgegeben wird yield. Rufen Sie nextjetzt noch einmal an:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Der Generator merkt sich, dass er angehalten wurde, yield valueund fährt von dort fort. Die nächste Nachricht wird gedruckt und die Suche nach der yieldAnweisung, die angehalten werden soll, wird erneut ausgeführt (aufgrund der whileSchleife).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.