12901 Stimmen

Was bewirkt das Schlüsselwort "yield"?

Wozu dient die yield Schlüsselwort in Python? Was bewirkt es?

Ich versuche zum Beispiel, folgenden Code zu verstehen 1 :

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? Wird sie erneut aufgerufen? Wann werden die nachfolgenden Aufrufe beendet?


1. Dieses Stück Code wurde von Jochen Schulz (jrschulz) geschrieben, der eine großartige Python-Bibliothek für metrische Räume entwickelt hat. Dies ist der Link zum vollständigen Quellcode: <a href="https://well-adjusted.de/~jrspieker/mspace/" rel="noreferrer">Modul mspace </a>.

18013voto

e-satis Punkte 547539

Um zu verstehen, was yield tut, müssen Sie verstehen, was Stromerzeuger sind. Und bevor Sie Generatoren verstehen können, müssen Sie verstehen Iterables .

Iterables

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

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

mylist ist ein iterierbar . Wenn Sie ein Listenverständnis verwenden, erstellen Sie eine Liste und damit eine Iterable:

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

Alles, was Sie verwenden können " for... in... on" ist eine Iterable; lists , strings , Dateien...

Diese Iterables sind praktisch, weil man sie beliebig oft lesen kann, aber man speichert alle Werte im Speicher, und das ist nicht immer das, was man will, wenn man viele Werte hat.

Stromerzeuger

Generatoren sind Iteratoren, eine Art Iterable können Sie nur einmal durchlaufen . Die 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 dasselbe, außer dass Sie () anstelle von [] . ABER, Sie kann nicht durchführen for i in mygenerator ein zweites Mal, da Generatoren nur einmal verwendet werden können: Sie berechnen 0, dann vergessen sie es und berechnen 1, und am Ende berechnen sie 4, einen nach dem anderen.

Ausbeute

yield ist ein Schlüsselwort, das wie folgt verwendet wird return mit dem Unterschied, dass die Funktion einen Generator zurückgibt.

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

Hier handelt es sich um ein nutzloses Beispiel, aber es ist praktisch, wenn Sie wissen, dass Ihre Funktion eine große Menge von Werten zurückgeben wird, die Sie nur einmal lesen müssen.

Zu meistern yield müssen Sie verstehen, dass Wenn Sie die Funktion aufrufen, wird der Code, den Sie in den Funktionsrumpf geschrieben haben, nicht ausgeführt. Die Funktion gibt nur das Generator-Objekt zurück, das ist ein bisschen knifflig.

Dann wird Ihr Code jedes Mal dort fortgesetzt, wo er aufgehört hat for verwendet den Generator.

Jetzt kommt der schwierige Teil:

Das erste Mal, dass die for das Generatorobjekt aufruft, das aus Ihrer Funktion erzeugt wurde, wird der Code in Ihrer Funktion von Anfang an ausgeführt, bis er auf yield zurück, dann wird der erste Wert der Schleife zurückgegeben. Bei jedem weiteren Aufruf wird eine weitere Iteration der Schleife, die Sie in die Funktion geschrieben haben, ausgeführt und der nächste Wert zurückgegeben. Dies wird so lange fortgesetzt, bis der Generator als leer angesehen wird, was dann der Fall ist, wenn die Funktion läuft, ohne auf yield . Das kann daran liegen, dass die Schleife zu Ende ist, oder daran, dass Sie eine Anforderung 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 eine Liste, aber die Liste erweitert sich, während die Schleife iteriert wird. Dies ist eine übersichtliche Methode, um all diese verschachtelten Daten durchzugehen, auch wenn sie etwas gefährlich ist, da sie in einer Endlosschleife enden kann. In diesem Fall, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) alle Werte des Generators ausschöpfen, aber while erzeugt immer wieder neue Generatorobjekte, die andere Werte als die vorherigen erzeugen, da sie nicht auf denselben Knoten angewendet werden.

  • Le site extend() Methode ist eine Listenobjektmethode, die eine iterable erwartet und deren 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 wird es ein Generator, was gut ist, weil:

  1. Sie brauchen die Werte nicht zweimal zu lesen.
  2. Vielleicht haben Sie viele Kinder und möchten nicht, dass sie alle im Speicher abgelegt werden.

Und es funktioniert, weil es Python nicht interessiert, ob das Argument einer Methode eine Liste ist oder nicht. Python erwartet Iterables, also funktioniert es mit Strings, Listen, Tupeln und Generatoren! Das nennt man Duck Typing 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 aufhören oder ein wenig weiter lesen, um einen fortgeschrittenen Einsatz eines Generators zu sehen:

Kontrolle der Erschöpfung eines Generators

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

Anmerkung: Für Python 3, verwenden Sie print(corner_street_atm.__next__()) o print(next(corner_street_atm))

Sie kann für verschiedene Dinge nützlich sein, z. B. für die Kontrolle des Zugangs zu einer Ressource.

Itertools, Ihr bester Freund

Das Modul itertools enthält spezielle Funktionen zur Manipulation von Iterables. Möchten Sie jemals einen Generator duplizieren? Zwei Generatoren verketten? Werte in einer verschachtelten Liste mit einem Einzeiler gruppieren? Map / Zip ohne eine weitere Liste zu erstellen?

Dann einfach import itertools .

Ein Beispiel? Schauen wir uns die möglichen Ankunftsreihenfolgen für ein Vier-Pferde-Rennen 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)]

Verstehen der inneren Mechanismen der Iteration

Iteration ist ein Prozess, der Iterabilien beinhaltet (Implementierung der __iter__() Methode) und Iteratoren (Implementierung der __next__() Methode). Iterables sind alle Objekte, von denen man einen Iterator erhalten kann. Iteratoren sind Objekte, die eine Iteration von Iterables ermöglichen.

Mehr dazu gibt es in diesem Artikel über comment for Schleifen arbeiten .

2548voto

user28409 Punkte 35934

Eine Abkürzung zum Verständnis yield

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

  1. Einfügen einer Zeile result = [] am Anfang der Funktion.
  2. Ersetzen Sie jede yield expr mit result.append(expr) .
  3. Einfügen einer Zeile return result am Ende der Funktion.
  4. Juhu - nicht mehr yield Aussagen! Lesen und verstehen Sie den Code.
  5. Vergleichen Sie die Funktion mit der ursprünglichen Definition.

Dieser Trick mag Ihnen eine Vorstellung von der Logik hinter der Funktion geben, aber was passiert eigentlich mit yield unterscheidet sich erheblich von dem, was beim listenbasierten Ansatz geschieht. In vielen Fällen ist der Yield-Ansatz wesentlich speichereffizienter und auch schneller. In anderen Fällen führt dieser Trick dazu, dass Sie 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, die Iterator-Protokoll - wenn Sie schreiben

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

Python führt die folgenden zwei Schritte durch:

  1. Ermittelt einen Iterator für mylist :

    Rufen Sie an. iter(mylist) -> dies gibt ein Objekt mit einer next() Methode (oder __next__() in Python 3).

    (Dies ist der Schritt, den die meisten Leute vergessen, Ihnen zu sagen)

  2. Verwendet den Iterator, um eine Schleife über die Elemente zu ziehen:

    Rufen Sie weiterhin die next() Methode auf den in Schritt 1 zurückgegebenen Iterator. Der Rückgabewert von next() ist zugeordnet zu x und der Schleifenkörper wird ausgeführt. Wenn eine Ausnahme StopIteration wird von innen heraus erhoben next() bedeutet dies, dass keine weiteren Werte im Iterator vorhanden sind und die Schleife beendet wird.

Die Wahrheit ist, dass Python die beiden oben genannten Schritte immer dann ausführt, wenn es das möchte überschleifen den Inhalt eines Objekts - es könnte also eine for-Schleife sein, aber es könnte auch ein Code wie otherlist.extend(mylist) (wobei otherlist ist eine Python-Liste).

Hier mylist ist ein iterierbar weil es das Iterator-Protokoll implementiert. In einer benutzerdefinierten Klasse können Sie das __iter__() Methode, um Instanzen Ihrer Klasse iterierbar zu machen. Diese Methode sollte ein Iterator . Ein Iterator ist ein Objekt mit einer next() Methode. Es ist möglich, beides zu implementieren __iter__() y next() in der gleichen Klasse, und haben __iter__() return self . Dies funktioniert in einfachen Fällen, aber nicht, wenn Sie zwei Iteratoren gleichzeitig über dasselbe Objekt laufen lassen wollen.

Das ist also das Iterator-Protokoll, viele Objekte implementieren dieses Protokoll:

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

Beachten Sie, dass eine for Schleife weiß nicht, mit welcher Art von Objekt sie es zu tun hat - sie folgt einfach dem Iterator-Protokoll und freut sich, ein Element nach dem anderen zu erhalten, wenn sie die next() . Eingebaute Listen geben ihre Elemente einzeln zurück, Wörterbücher geben die Tasten eine nach der anderen, Dateien geben die Zeilen einen nach dem anderen, usw. Und die Generatoren kehren zurück... nun, das ist der Punkt, an dem yield kommt herein:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Anstelle von yield Aussagen, wenn Sie drei return Aussagen in f123() würde nur die erste ausgeführt werden, und die Funktion würde beendet. Aber f123() ist keine gewöhnliche Funktion. Wenn f123() aufgerufen wird, ist es nicht einen der Werte in den Yield-Anweisungen zurückgeben! Sie gibt ein Generator-Objekt zurück. Außerdem wird die Funktion nicht wirklich beendet - sie geht in einen Schwebezustand über. Wenn die for Schleife versucht, über das Generatorobjekt zu laufen, wird die Funktion in der nächsten Zeile nach der yield von dem er zuvor zurückgekehrt ist, führt die nächste Codezeile aus, in diesem Fall eine yield Anweisung, und gibt diese als nächstes Element zurück. Dies geschieht, bis die Funktion beendet wird, woraufhin der Generator StopIteration und die Schleife wird beendet.

Das Generator-Objekt ist also so etwas wie ein Adapter - an einem Ende stellt es das Iterator-Protokoll aus, indem es __iter__() y next() Methoden zur Erhaltung der for Schleife glücklich. Am anderen Ende der Schleife wird die Funktion jedoch nur so lange ausgeführt, bis sie den nächsten Wert liefert, und dann wieder in den Schwebezustand versetzt.

Warum Generatoren verwenden?

Normalerweise können Sie einen Code schreiben, der keine Generatoren verwendet, aber die gleiche Logik implementiert. Eine Möglichkeit ist der "Trick" mit der temporären Liste, den ich bereits erwähnt habe. Das wird nicht in allen Fällen funktionieren, z. B. wenn Sie Endlosschleifen haben, oder es kann zu einer ineffizienten Nutzung des Speichers führen, wenn Sie eine sehr lange Liste haben. Der andere Ansatz besteht darin, eine neue iterable Klasse SomethingIter zu implementieren, die den Zustand in Instanzmitgliedern hält und den nächsten logischen Schritt in ihrer next() (oder __next__() in Python 3) Methode. Je nach Logik kann der Code innerhalb der next() Methode kann am Ende sehr komplex aussehen und fehleranfällig sein. Hier bieten Generatoren eine saubere und einfache Lösung.

775voto

Jason Baker Punkte 180981

Betrachten Sie es einmal so:

Ein Iterator ist nur ein schick klingender Begriff für ein Objekt, das eine next() Methode. Eine ertragsorientierte Funktion sieht also in etwa so aus:

Originalfassung:

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

for i in some_function():
    print i

Das 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

Weitere Informationen darüber, was hinter den Kulissen geschieht, finden Sie im for Schleife kann wie folgt umgeschrieben werden:

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

Ergibt das mehr Sinn oder verwirrt es Sie nur noch mehr? :)

Ich sollte anmerken, dass dies es eine grobe Vereinfachung zu Illustrationszwecken :)

664voto

ninjagecko Punkte 82995

Le site yield wird auf zwei einfache Fakten reduziert:

  1. Wenn der Compiler die yield Stichwort überall innerhalb einer Funktion, kehrt diese Funktion nicht mehr über die return Erklärung. Stattdessen es sofort gibt eine träges Objekt "Pendenzenliste ein Generator genannt
  2. Ein Generator ist iterierbar. Was ist ein iterierbar ? Es ist so etwas wie ein list o set o range oder Diktatansicht, mit einer eingebautes Protokoll für den Besuch jedes Elements in einer bestimmten Reihenfolge .

Kurz und bündig: ein Generator ist eine faule, inkrementell ablaufende Liste et yield Anweisungen können Sie die Funktionsnotation zur Programmierung der Listenwerte verwenden sollte der Generator schrittweise ausspucken.

generator = myYieldingFunction(...)  # basically a list (but lazy)
x = list(generator)  # evaluate every element into a list

   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

Grundsätzlich gilt, dass immer dann, wenn die yield Anweisung angetroffen wird, hält die Funktion an und speichert ihren Zustand, dann gibt sie "den nächsten Rückgabewert in der 'Liste'" gemäß dem Python-Iterator-Protokoll aus (an ein syntaktisches Konstrukt wie eine for-Schleife, die wiederholt next() und fängt eine StopIteration Ausnahme, usw.). Vielleicht sind Sie schon einmal auf Generatoren mit Generatorausdrücke Generatorfunktionen sind leistungsfähiger, weil Sie Argumente an die angehaltene Generatorfunktion zurückgeben und sie zur Implementierung von Coroutines verwenden können. Mehr dazu später.


Grundlegendes Beispiel ("Liste")

Definieren wir eine Funktion makeRange das ist genau wie Pythons range . Aufruf von makeRange(n) GIBT 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 den Generator zu zwingen, seine ausstehenden Werte sofort zurückzugeben, können Sie ihn in list() (genau wie bei jeder anderen Iterablen):

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

# return a list                  #  # return a generator
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
    return TO_RETURN             # 

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

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


Wie Sie Generatoren verwenden können

Eine Iterable ist der letzte Teil eines Listenverständnisses, und alle Generatoren sind iterabel, so dass sie oft auf diese Weise verwendet werden:

#                  < 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 der itertools Modul (achten Sie darauf, dass Sie chain.from_iterable statt chain wenn dies gerechtfertigt ist). Sie könnten zum Beispiel sogar Generatoren verwenden, um unendlich lange faule Listen zu implementieren wie itertools.count() . Sie könnten Ihr eigenes System implementieren def enumerate(iterable): zip(count(), iterable) oder alternativ mit der Option yield Schlüsselwort in einer while-Schleife.

Bitte beachten Sie: Generatoren können noch für viele andere Dinge verwendet werden, wie zum Beispiel Implementierung von Koroutinen oder nicht-deterministische Programmierung oder andere elegante Dinge. Der hier vorgestellte Standpunkt der "faulen Listen" ist jedoch die häufigste Anwendung, die Sie finden werden.


Hinter den Kulissen

So funktioniert das "Python-Iterationsprotokoll". Das heißt, was passiert, wenn Sie Folgendes tun list(makeRange(5)) . Dies ist das, was ich zuvor als "faule, inkrementelle Liste" bezeichnet habe.

>>> x=iter(range(5))
>>> next(x)  # calls x.__next__(); x.next() is deprecated
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 Objekte .__next__() Funktion, die ein Teil des "Iterationsprotokolls" ist und bei allen Iteratoren zu finden ist. Sie können manuell die next() Funktion (und andere Teile des Iterationsprotokolls), um ausgefallene Dinge zu implementieren, normalerweise auf Kosten der Lesbarkeit, also versuchen Sie, das zu vermeiden...


Koroutinen

Koroutine Beispiel:

def interactiveProcedure():
    userResponse = yield makeQuestionWebpage()
    print('user response:', userResponse)
    yield 'success'

coroutine = interactiveProcedure()
webFormData = next(coroutine)  # same as .send(None)
userResponse = serveWebForm(webFormData)

# ...at some point later on web form submit...

successStatus = coroutine.send(userResponse)

Minutien

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

In Python-Sprache bedeutet ein iterierbar ist ein beliebiges Objekt, das das Konzept einer for-Schleife versteht, wie eine Liste [1,2,3] und ein Iterator ist eine spezifische Instanz der angeforderten for-Schleife wie [1,2,3].__iter__() . A Generator ist genau dasselbe wie jeder Iterator, abgesehen von der Art, 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 werden), erhalten Sie nur eine Kopie von sich selbst.

Für den 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 Das heißt, er ist einmalig zu verwenden. Wenn Sie sie wiederverwenden wollen, sollten Sie myRange(...) wieder. Wenn Sie das Ergebnis zweimal verwenden müssen, wandeln Sie das Ergebnis in eine Liste um und speichern es in einer Variablen x = list(myRange(5)) . Diejenigen, die unbedingt einen Generator klonen müssen (z. B. wenn sie erschreckend hakelige Metaprogrammierung betreiben), können Folgendes verwenden itertools.tee ( funktioniert noch in Python 3 ), wenn dies unbedingt erforderlich ist, da die kopierbarer Iterator Python PEP-Standardvorschlag wurde verschoben.

572voto

Was bedeutet die yield Schlüsselwort in Python tun?

Antwort Gliederung/Zusammenfassung

  • Eine Funktion mit yield wenn er angerufen wird, gibt eine Stromerzeuger .
  • Generatoren sind Iteratoren, denn sie implementieren die Iterator-Protokoll so dass man sie iterativ durchgehen kann.
  • Ein Generator kann auch sein gesendete Informationen und ist damit konzeptionell ein Koroutine .
  • In Python 3 können Sie delegieren von einem Generator zum anderen in beide Richtungen mit yield from .
  • (Im Anhang werden einige Antworten kritisiert, darunter die oberste, und es wird die Verwendung von return in einem Generator).

Generatoren:

yield ist nur innerhalb einer Funktionsdefinition zulässig, und die Einbeziehung von yield in einer Funktionsdefinition bewirkt, dass sie einen Generator zurückgibt.

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 gefroren an der Stelle der Ausbeute. Wenn der Generator aufgerufen wird (die Methoden werden weiter unten behandelt), wird die Ausführung fortgesetzt und beim nächsten Yield eingefroren.

yield bietet eine einfachen Weg der Implementierung des Iterator-Protokolls die durch die beiden folgenden Methoden definiert werden: __iter__ y next (Python 2) oder __next__ (Python 3). Diese beiden Methoden machen ein Objekt zu einem Iterator, den man mit der Methode "type-check" überprüfen kann. Iterator Abstrakte Basis Klasse aus der collections Modul.

>>> 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 Subtyp des Iterators:

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

Und wenn es nötig ist, können wir die Eingabe wie folgt überprüfen:

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

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

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

Sie müssen eine neue erstellen, wenn Sie die Funktion wieder nutzen wollen (siehe Fußnote 2):

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

Man kann zum Beispiel Daten programmatisch ausgeben:

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

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

def func(an_iterable):
    yield from an_iterable

Allerdings, yield from ermöglicht auch die Delegation an Untergeneratoren, was im folgenden Abschnitt über die kooperative Delegation mit Sub-Coroutinen erläutert wird.

Koroutinen:

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

Hier ist ein Beispiel, beachten Sie die received 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)

Zunächst müssen wir den Generator mit der eingebauten Funktion in die Warteschlange stellen, next . Es wird ruft die entsprechende next o __next__ Methode, abhängig von der Version von Python, die Sie verwenden:

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

Jetzt können wir Daten an den Generator senden. ( Senden von None ist dasselbe wie der Aufruf next .) :

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

Kooperative Delegation an Sub-Coroutine mit yield from

Erinnern Sie sich nun daran, dass yield from ist in Python 3 verfügbar. Damit können wir Coroutines an eine Subcoroutine delegieren:

def money_manager(expected_rate):
    # must receive deposited value from .send():
    under_management = yield                   # yield None to start.
    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'''
            raise
        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'''
    # must queue up manager:
    next(manager)      # <- same as manager.send(None)
    # This is where we send the initial deposit to the manager:
    manager.send(deposited)
    try:
        yield from manager
    except GeneratorExit:
        return manager.close()  # delegate?

Und jetzt können wir die Funktionalität an einen Untergenerator delegieren, der dann von einem Generator wie oben verwendet werden:

my_manager = money_manager(.06)
my_account = investment_account(1000, my_manager)
first_year_return = next(my_account) # -> 60.0

Simulieren Sie nun die Aufstockung des Kontos um weitere 1.000 plus die Rendite des Kontos (60,0):

next_year_return = my_account.send(first_year_return + 1000)
next_year_return # 123.6

Sie können mehr über die genaue Semantik von yield from sur PEP 380.

Andere Methoden: schließen und werfen

Le site close Methode wirft GeneratorExit an dem Punkt, an dem die Funktion Ausführung eingefroren wurde. Dies wird auch aufgerufen von __del__ damit Sie jeden Bereinigungscode dort einfügen können, wo Sie die GeneratorExit :

my_account.close()

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

import sys
try:
    raise ValueError
except:
    my_manager.throw(*sys.exc_info())

Erhöht:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 6, in money_manager
  File "<stdin>", line 2, in <module>
ValueError

Schlussfolgerung

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

Was bedeutet die yield Schlüsselwort in Python tun?

Es stellt sich heraus, dass yield macht eine Menge. Ich bin sicher, ich könnte noch mehr ausführliche Beispiele hinzufügen. Wenn Sie mehr wollen oder konstruktive Kritik haben, lassen Sie es mich wissen, indem Sie einen Kommentar unten.


Anhang:

Kritik an der Top/Akzeptierten Antwort**

  • Es herrscht Verwirrung darüber, was eine iterierbar nur anhand einer Liste als Beispiel. Siehe meine Referenzen oben, aber zusammenfassend: eine iterable hat eine __iter__ Methode, die eine Iterator . Eine Iterator bietet eine .next (Python 2 oder .__next__ (Python 3)-Methode, die implizit aufgerufen wird von for Schleifen, bis es zu einer StopIteration Und wenn sie es einmal getan hat, wird sie es auch weiterhin tun.
  • Anschließend wird ein Generatorausdruck verwendet, um zu beschreiben, was ein Generator ist. Da ein Generator einfach eine bequeme Möglichkeit ist, eine Iterator Das verwirrt die Sache nur, und wir sind immer noch nicht bei der yield Teil.
  • Unter Kontrolle der Erschöpfung eines Generators ruft er die .next Methode, wenn er stattdessen die eingebaute Funktion verwenden sollte, next . Es wäre eine geeignete Umleitungsebene, da sein Code in Python 3 nicht funktioniert.
  • Itertools? Dies war nicht relevant für das, was yield überhaupt nicht tut.
  • Keine Diskussion über die Methoden, die yield bietet zusammen mit der neuen Funktionalität yield from in Python 3. Die erste/akzeptierte Antwort ist eine sehr unvollständige Antwort.

Kritik an der Antwort vorschlagen yield in einem Generator Ausdruck oder Verständnis.

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 yield ein Ausdruck ist, wurde es von einigen als interessant angepriesen, es in Comprehensions oder Generatorausdrücken zu verwenden - obwohl kein besonders guter Anwendungsfall genannt wurde.

Die CPython-Kernentwickler sind Erörterung der Abschreibung seiner Wertberichtigung . Hier ist ein entsprechender Beitrag aus der Mailingliste:

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

Am Sun, 29 Jan 2017 at 16:39 schrieb Craig Rodrigues:

Ich bin mit beiden Ansätzen einverstanden. Die Dinge so zu belassen, wie sie in Python 3 sind ist nicht gut, IMHO.

Ich stimme dafür, dass es ein SyntaxError ist, da Sie nicht das bekommen, was Sie von der Syntax erwarten. der Syntax erwarten.

Ich stimme zu, dass dies ein vernünftiger Ort ist, an dem wir landen sollten, da jeder Code Code, der sich auf das aktuelle Verhalten verlässt, ist wirklich zu clever, um wartbar zu sein.

Was den Weg dorthin betrifft, so werden wir uns wahrscheinlich wünschen:

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

Prost, Nick.

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

Außerdem gibt es eine offene Frage (10544) die in diese Richtung zu weisen scheint niemals eine gute Idee ist (PyPy, eine in Python geschriebene Python-Implementierung, gibt bereits Syntaxwarnungen aus).

Solange uns die Entwickler von CPython nicht eines Besseren belehren, ist das nicht der Fall: Legen Sie nicht yield in einem Generator Ausdruck oder Verständnis.

Le site return Anweisung in einem Generator

Unter Python 2 :

In einer Generatorfunktion ist die return Anweisung darf nicht die Anweisung expression_list . In diesem Zusammenhang ist eine bloße return zeigt an, dass der Generator fertig ist und verursacht StopIteration angehoben werden.

Eine expression_list ist im Grunde eine beliebige Anzahl von Ausdrücken, die durch Kommas getrennt sind - in Python 2 können Sie den Generator mit return , aber Sie können keinen Wert zurückgeben.

Unter Python 3 :

In einer Generatorfunktion ist die return Anweisung zeigt an, dass der Generator fertig ist und wird StopIteration ausgelöst werden. Der zurückgegebene Wert (falls vorhanden) wird als Argument für die Konstruktion von StopIteration und wird zum StopIteration.value Attribut.

Fußnoten

  1. In dem Vorschlag wurde auf die Sprachen CLU, Sather und Icon verwiesen um das Konzept der Generatoren in Python einzuführen. Die allgemeine Idee ist dass eine Funktion einen internen Zustand beibehalten und Zwischenergebnisse liefern kann Datenpunkte auf Anfrage des Benutzers liefert. Dies versprach zu sein <a href="https://www.python.org/dev/peps/pep-0255/" rel="noreferrer">überragend in der Leistung gegenüber anderen Ansätzen, einschließlich Python-Threading </a>die auf einigen Systemen gar nicht verfügbar ist.

  2. Dies bedeutet zum Beispiel, dass <code>range</code> Objekte sind nicht <code>Iterator</code> s, auch wenn sie wiederholbar sind, weil sie wiederverwendet werden können. Wie Listen sind ihre <code>iter</code> Methoden geben Iterator-Objekte zurück.

yield wurde ursprünglich als Anweisung eingeführt, was bedeutet, dass es nur am Anfang einer Zeile in einem Codeblock erscheinen konnte. Jetzt yield erzeugt einen Ertragsausdruck. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Diese Änderung war vorgeschlagen damit ein Benutzer Daten in den Generator senden kann, so wie er wie man sie empfangen könnte. Um Daten zu senden, muss man sie etwas zuordnen können, und und dafür reicht eine Anweisung einfach nicht aus.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X