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

#!/usr/bin/python # -*- coding: UTF-8 -*- def fab(max): n, a, b = 0, 0, 1 while n < max: print b a, b = b, a + b n = n + 1 fab(5)

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

#!/usr/bin/python # -*- coding: UTF-8 -*- def fab(max): n, a, b = 0, 0, 1 L = [] while n < max: L.append(b) a, b = b, a + b n = n + 1 return L for n in fab(5): print n

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

#!/usr/bin/python # -*- coding: UTF-8 -*- class Fab(object): def __init__(self, max): self.max = max self.n, self.a, self.b = 0, 0, 1 def __iter__(self): return self def next(self): if self.n < self.max: r = self.b self.a, self.b = self.b, self.a + self.b self.n = self.n + 1 return r raise StopIteration() for n in Fab(5): print n

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

#!/usr/bin/python # -*- coding: UTF-8 -*- def fab(max): n, a, b = 0, 0, 1 while n < max: yield b # Verwendung von yield # print b a, b = b, a + b n = n + 1 for n in fab(5): print n

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

>>>f = fab(5) >>> f.next() 1 >>> f.next() 1 >>> f.next() 2 >>> f.next() 3 >>> f.next() 5 >>> f.next() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration

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

>>>from inspect import isgeneratorfunction >>> isgeneratorfunction(fab) True

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

>>>import types >>> isinstance(fab, types.GeneratorType) False >>> isinstance(fab(5), types.GeneratorType) True
fab ist nicht iterierbar, während fab(5) iterierbar ist:
>>>from collections import Iterable >>> isinstance(fab, Iterable) False >>> isinstance(fab(5), Iterable) True

Jeder Aufruf der Funktion fab erzeugt eine neue Generatorinstanz, und die Instanzen beeinflussen sich nicht gegenseitig:

>>>f1 = fab(3) >>> f2 = fab(5) >>> print 'f1:', f1.next() f1: 1 >>> print 'f2:', f2.next() f2: 1 >>> print 'f1:', f1.next() f1: 1 >>> print 'f2:', f2.next() f2: 1 >>> print 'f1:', f1.next() f1: 2 >>> print 'f2:', f2.next() f2: 2 >>> print 'f2:', f2.next() f2: 3 >>> print 'f2:', f2.next() f2: 5

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

def read_file(fpath): BLOCK_SIZE = 1024 with open(fpath, 'rb') as f: while True: block = f.read(BLOCK_SIZE) if block: yield block else: return

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/