Sie haben vielleicht gehört, dass Funktionen mit yield in Python Generatoren genannt werden. Was ist ein Generator?
Lassen Sie uns Generatoren vorerst beiseitelegen und anhand eines gängigen Programmierproblems das Konzept von yield demonstrieren.
Wie man die Fibonacci-Folge erzeugt
Die Fibonacci-Folge ist eine sehr einfache rekursive Folge. Abgesehen von der ersten und zweiten Zahl kann jede Zahl durch Addition der beiden vorherigen Zahlen erhalten werden. Die ersten N Zahlen der Fibonacci-Folge mit einem Computerprogramm auszugeben, ist ein sehr einfaches Problem, und viele Anfänger können die folgende Funktion leicht schreiben:
Listing 1. Einfach die ersten N Zahlen der Fibonacci-Folge ausgeben
Beispiel
Wenn wir den obigen Code ausführen, erhalten wir die folgende Ausgabe:
1 1 2 3 5
Das Ergebnis ist in Ordnung, aber erfahrene Entwickler werden darauf hinweisen, dass die direkte Verwendung von print in der Funktion fab zum Ausgeben von Zahlen die Funktion schlecht wiederverwendbar macht, weil die Funktion fab None zurückgibt und andere Funktionen die von dieser Funktion erzeugte Sequenz nicht erhalten können.
Um die Wiederverwendbarkeit der Funktion fab zu verbessern, ist es am besten, die Sequenz nicht direkt auszugeben, sondern eine Liste zurückzugeben. Das Folgende ist die zweite Version der umgeschriebenen Funktion fab:
Listing 2. Zweite Version zur Ausgabe der ersten N Zahlen der Fibonacci-Folge
Beispiel
Sie können die von der Funktion fab zurückgegebene Liste folgendermaßen ausgeben:
1 1 2 3 5
Die umgeschriebene Funktion fab erfüllt die Anforderung an die Wiederverwendbarkeit, indem sie eine Liste zurückgibt, aber erfahrenere Entwickler werden darauf hinweisen, dass der von dieser Funktion während der Ausführung belegte Speicher mit steigendem Parameter max zunimmt. Um den Speicherverbrauch zu kontrollieren, ist es am besten, keine Liste zu verwenden
um Zwischenergebnisse zu speichern, sondern stattdessen ein iterierbares Objekt zu durchlaufen. Zum Beispiel in Python 2.x der Code:
Listing 3. Durchlaufen eines iterierbaren Objekts
for i in range(1000): pass
wird eine Liste mit 1000 Elementen erzeugen, während der Code:
for i in xrange(1000): pass
wird keine Liste mit 1000 Elementen erzeugen, sondern gibt bei jeder Iteration den nächsten Wert zurück und belegt dabei nur sehr wenig Speicherplatz. Das liegt daran, dass xrange keine Liste zurückgibt, sondern ein iterierbares Objekt.
Mithilfe von Iterables können wir die fab-Funktion als eine Klasse umschreiben, die Iterables unterstützt. Das Folgende ist die dritte Version von Fab:
Listing 4. Dritte Version
Beispiel
Die Fab-Klasse gibt kontinuierlich die nächste Zahl der Sequenz über next() zurück, und der Speicherverbrauch bleibt konstant:
1 1 2 3 5
Diese unter Verwendung einer Klasse neu geschriebene Version ist jedoch weitaus weniger prägnant als die erste Version der Funktion fab. Wenn wir die Prägnanz der ersten Version der Funktion fab beibehalten und gleichzeitig den iterierbaren Effekt erzielen wollen, kommt yield ins Spiel:
Listing 5. Vierte Version mit yield
Beispiel
Im Vergleich zur ersten Version ändert die vierte Version von fab einfach print b in yield b, wodurch der iterierbare Effekt erzielt wird und die Prägnanz erhalten bleibt.
Der Aufruf der vierten Version von fab ist genau derselbe wie der Aufruf der zweiten Version von fab:
1 1 2 3 5
Einfach ausgedrückt besteht die Rolle von yield darin, eine Funktion in einen Generator zu verwandeln. Eine Funktion mit yield ist keine gewöhnliche Funktion mehr. Der Python-Interpreter behandelt sie als Generator. Der Aufruf fab(5) führt nicht die Funktion fab aus, sondern gibt ein iterierbares Objekt zurück! Wenn die for-Schleife ausgeführt wird, führt jede Iteration den Code innerhalb der Funktion fab aus. Wenn sie yield b erreicht, gibt die Funktion fab einen Iterationswert zurück. Bei der nächsten Iteration wird der Code ab der Anweisung nach yield b weiter ausgeführt, und die lokalen Variablen der Funktion erscheinen genau wie vor der letzten Unterbrechung. Die Funktion wird dann weiter ausgeführt, bis sie erneut auf yield trifft.
Sie können auch manuell die Methode next() von fab(5) aufrufen (weil fab(5) ein Generatorobjekt ist, das eine Methode next() besitzt), wodurch wir den Ausführungsfluss von fab klarer sehen können:
Listing 6. Ausführungsablauf
Wenn die Funktion die Ausführung beendet, löst der Generator automatisch eine StopIteration-Ausnahme aus, was anzeigt, dass die Iteration abgeschlossen ist. In einer for-Schleife muss die StopIteration-Ausnahme nicht behandelt werden; die Schleife wird normal beendet.
Wir können die folgenden Schlussfolgerungen ziehen:
Eine Funktion mit yield ist ein Generator. Im Gegensatz zu gewöhnlichen Funktionen sieht das Erzeugen eines Generators wie ein Funktionsaufruf aus, aber es wird kein Funktionscode ausgeführt, bis next() darauf aufgerufen wird (in einer for-Schleife wird next() automatisch aufgerufen). Obwohl der Ausführungsablauf weiterhin dem Fluss der Funktion folgt, wird er jedes Mal unterbrochen, wenn er eine yield-Anweisung erreicht, und gibt einen Iterationswert zurück. Wenn die Ausführung das nächste Mal fortgesetzt wird, wird sie mit der Anweisung nach yield fortgesetzt. Es sieht so aus, als wäre eine Funktion während der normalen Ausführung mehrmals durch yield unterbrochen worden, und jede Unterbrechung gibt den aktuellen Iterationswert über yield zurück.
Die Vorteile von yield sind offensichtlich. Wenn man eine Funktion als Generator neu schreibt, erhält sie Iterationsfähigkeit. Verglichen damit, eine Klasseninstanz zum Speichern des Zustands und zur Berechnung des nächsten Werts für next() zu verwenden, ist dies nicht nur prägnanter im Code, sondern auch außergewöhnlich klar im Ausführungsablauf.
Wie können wir feststellen, ob eine Funktion eine spezielle Generatorfunktion ist? Wir können isgeneratorfunction verwenden, um dies zu prüfen:
Listing 7. Verwendung von isgeneratorfunction zur Prüfung
Wir sollten darauf achten, zwischen fab und fab(5) zu unterscheiden. fab ist eine Generatorfunktion, während fab(5) ein Generator ist, der durch Aufrufen von fab zurückgegeben wird, genau wie der Unterschied zwischen einer Klassendefinition und einer Klasseninstanz:
Listing 8. Klassendefinition und Klasseninstanz
Jeder Aufruf der Funktion fab erzeugt eine neue Generatorinstanz, und die Instanzen beeinflussen sich nicht gegenseitig:
Die Rolle von return
In einer Generatorfunktion wird, wenn kein return vorhanden ist, standardmäßig bis zum Abschluss der Funktion ausgeführt. Wenn während der Ausführung return auftritt, wird direkt StopIteration ausgelöst, um die Iteration zu beenden.
Ein weiteres Beispiel
Ein weiteres Beispiel für yield stammt aus dem Dateilesen. Wenn Sie die Methode read() direkt auf einem Dateiobjekt aufrufen, kann dies unvorhersehbaren Speicherverbrauch verursachen. Ein guter Ansatz ist, einen Puffer fester Länge zu verwenden, um den Dateiinhalt kontinuierlich zu lesen. Durch yield müssen wir keine Iterationsklasse für das Dateilesen mehr schreiben und können das Dateilesen einfach implementieren:
Listing 9. Ein weiteres Beispiel für yield
Beispiel
Das Obige führt nur kurz in die grundlegenden Konzepte und die Verwendung von yield ein. yield hat in Python 3 leistungsfähigere Verwendungsmöglichkeiten, die wir in nachfolgenden Artikeln besprechen.
Hinweis: Der gesamte Code in diesem Artikel wurde getestet und in Python 2.7 erfolgreich ausgeführt.
Original-URL: https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/