Heim > Artikel > Backend-Entwicklung > Erweiterte Verwendung von Iteratoren und Generatoren in Python
Iterator
Ein Iterator ist ein Objekt, das sich an das Iterationsprotokoll hält – im Grunde bedeutet es, dass es eine nächste Methode hat, die bei Aufruf die Sequenz Nächstes Projekt zurückgibt. Wenn keine zurückzugebenden Elemente vorhanden sind, lösen Sie die StopIteration-Ausnahme aus.
Iterationsobjekte ermöglichen eine Schleife. Es behält den Status (die Position) einer einzelnen Iteration bei, oder aus einer anderen Perspektive ist jedes Mal, wenn die Sequenz eine Schleife durchläuft, ein Iterationsobjekt erforderlich. Das bedeutet, dass wir dieselbe Sequenz mehr als einmal durchlaufen können. Durch die Trennung der Iterationslogik von der Sequenz erhalten wir mehr Möglichkeiten zur Iteration.
Der Aufruf der __iter__-Methode eines Containers zum Erstellen eines Iteratorobjekts ist der direkteste Weg, Iteratoren zu beherrschen. Die Funktion iter erspart uns einige Tastatureingaben.
>>> nums = [1,2,3] # note that ... varies: these are different objects >>> iter(nums) <listiterator object at ...> >>> nums.__iter__() <listiterator object at ...> >>> nums.__reversed__() <listreverseiterator object at ...> >>> it = iter(nums) >>> next(it) # next(obj) simply calls obj.next() 1 >>> it.next() 2 >>> next(it) 3 >>> next(it) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Bei Verwendung innerhalb einer Schleife wird StopIteration akzeptiert und stoppt die Schleife. Bei einem expliziten Aufruf sehen wir jedoch, dass der Zugriff darauf eine Ausnahme auslöst, sobald die Iteratorelemente erschöpft sind.
Verwenden Sie die for...in-Schleife und verwenden Sie auch die __iter__-Methode. Dadurch können wir transparent mit der Iteration über eine Sequenz beginnen. Wenn wir jedoch bereits über einen Iterator verfügen, möchten wir diesen auf ähnliche Weise in einer for-Schleife verwenden können. Um dies zu erreichen, verfügt der Iterator zusätzlich zu next auch über eine Methode __iter__, um den Iterator selbst (self) zurückzugeben.
Unterstützung für Iteratoren in Python ist allgegenwärtig: Alle Sequenz- und ungeordneten Container in der Standardbibliothek werden unterstützt. Dieses Konzept wurde auch auf andere Dinge ausgeweitet: Beispielsweise unterstützt das Dateiobjekt die Iteration von Zeilen.
>>> f = open('/etc/fstab') >>> f is f.__iter__() True
Datei selbst ist ein Iterator und ihre __iter__-Methode erstellt kein separates Objekt: Es ist nur sequentielles Lesen mit einem Thread zulässig.
Ausdruck generieren
Die zweite Möglichkeit, ein iterierbares Objekt zu erstellen, ist die Verwendung eines Generatorausdrucks, der Grundlage des Listenverständnisses. Um die Übersichtlichkeit zu erhöhen, werden generierte Ausdrücke immer in Klammern oder Ausdrücke eingeschlossen. Wenn Klammern verwendet werden, wird ein Generator-Iterator erstellt. Bei eckigen Klammern wird dieser Vorgang „kurzgeschlossen“ und wir erhalten eine Liste.
>>> (i for i in nums) <generator object <genexpr> at 0x...> >>> [i for i in nums] [1, 2, 3] >>> list(i for i in nums) [1, 2, 3]
In Python 2.7 und 3.x wurde die Listenausdruckssyntax auf Wörterbuch- und Mengenausdrücke erweitert. Eine Menge wird erstellt, wenn ein generierter Ausdruck in geschweifte Klammern eingeschlossen wird. Ein Wörterbuch-Diktat wird erstellt, wenn ein Ausdruck Schlüssel-Wert-Paare der Form Schlüssel:Wert enthält:
>>> {i for i in range(3)} set([0, 1, 2]) >>> {i:i**2 for i in range(3)} {0: 0, 1: 1, 2: 4}
Wenn Sie das Pech haben, darin stecken zu bleiben eine alte Version von Python, diese Syntax ist etwas schlecht:
>>> set(i for i in 'abc') set(['a', 'c', 'b']) >>> dict((i, ord(i)) for i in 'abc') {'a': 97, 'c': 99, 'b': 98}
Das Generieren von Ausdrücken ist natürlich recht einfach. Es gibt nur einen erwähnenswerten Fallstrick: Die Indexvariable (i) leckt in Python-Versionen vor Version 3.
Generatoren
Generatoren sind Funktionen, die eine Liste von Ergebnissen statt eines einzelnen Werts erzeugen.
Die dritte Möglichkeit, ein iterierbares Objekt zu erstellen, besteht darin, eine Generatorfunktion aufzurufen. Ein Generator ist eine Funktion, die das Schlüsselwort yield enthält. Es ist erwähnenswert, dass das bloße Vorhandensein dieses Schlüsselworts die Art der Funktion völlig verändert: Die yield-Anweisung muss nicht aufgerufen oder überhaupt zugänglich sein. Aber lassen Sie die Funktion zu einem Generator werden. Wenn eine Funktion aufgerufen wird, werden die darin enthaltenen Anweisungen ausgeführt. Und wenn ein Generator aufgerufen wird, stoppt die Ausführung vor der ersten darin enthaltenen Anweisung. Ein Aufruf eines Generators erstellt ein Generatorobjekt, das an das Iterationsprotokoll angehängt ist. Genau wie bei regulären Funktionen sind gleichzeitige und rekursive Aufrufe zulässig.
Beim Aufruf von next wird die Funktion bis zur ersten Ausbeute ausgeführt. Jedes Mal, wenn eine Yield-Anweisung auftritt, wird ein Wert zurückgegeben, der als nächstes zurückgegeben wird. Nachdem die Yield-Anweisung ausgeführt wurde, wird die Ausführung der Funktion gestoppt.
>>> def f(): ... yield 1 ... yield 2 >>> f() <generator object f at 0x...> >>> gen = f() >>> gen.next() 1 >>> gen.next() 2 >>> gen.next() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Lassen Sie uns den gesamten Verlauf eines einzelnen Generatorfunktionsaufrufs durchgehen.
>>> def f(): ... print("-- start --") ... yield 3 ... print("-- middle --") ... yield 4 ... print("-- finished --") >>> gen = f() >>> next(gen) -- start -- 3 >>> next(gen) -- middle -- 4 >>> next(gen) -- finished -- Traceback (most recent call last): ... StopIteration
Im Vergleich zur Ausführung von f() in einer regulären Funktion, um print sofort auszuführen, wird gen zugewiesen, ohne dass Anweisungen im Funktionskörper ausgeführt werden. Erst beim nächsten Aufruf von gen.next() werden die Anweisungen bis zum ersten Yield-Teil ausgeführt. Die zweite Anweisung gibt „Mitte“ aus und stoppt die Ausführung, wenn der zweite Yield auftritt. Der dritte Druck ist abgeschlossen – und am Ende der Funktion wird eine Ausnahme ausgelöst, da kein Yield vorhanden ist.
Was passiert, wenn die Kontrolle an den Aufrufer zurückgegeben wird, nachdem die Funktion nachgibt? Der Zustand jedes Generators wird im Generatorobjekt gespeichert. Von diesem Punkt an sieht die Generatorfunktion so aus, als würde sie in einem separaten Thread ausgeführt, aber das ist nur eine Illusion: Die Ausführung erfolgt streng Single-Threaded, aber der Interpreter behält und speichert den Status zwischen den nächsten Wertanforderungen.
Warum sind Generatoren nützlich? Wie im Abschnitt über Iteratoren betont, sind Generatorfunktionen nur eine weitere Möglichkeit, iterierbare Objekte zu erstellen. Alles, was durch die Yield-Anweisung vervollständigt werden kann, kann auch durch die nächste Methode vervollständigt werden. Allerdings bietet die Verwendung von Funktionen, mit denen der Interpreter auf magische Weise Iteratoren erstellen kann, Vorteile. Eine Funktion kann viel kürzer sein als eine Klassendefinition, die die Methoden next und __iter__ erfordert. Noch wichtiger ist, dass Generatorautoren Anweisungen, die auf lokale Variablen lokalisiert sind, leichter verstehen können, als Instanzeigenschaften des Iteratorobjekts zwischen aufeinanderfolgenden nächsten Aufrufen übergeben zu müssen.
还有问题是为何迭代器有用?当一个迭代器用来驱动循环,循环变得简单。迭代器代码初始化状态,决定是否循环结束,并且找到下一个被提取到不同地方的值。这凸显了循环体——最值得关注的部分。除此之外,可以在其它地方重用迭代器代码。
双向通信
每个yield语句将一个值传递给调用者。这就是为何PEP 255引入生成器(在Python2.2中实现)。但是相反方向的通信也很有用。一个明显的方式是一些外部(extern)语句,或者全局变量或共享可变对象。通过将先前无聊的yield语句变成表达式,直接通信因PEP 342成为现实(在2.5中实现)。当生成器在yield语句之后恢复执行时,调用者可以对生成器对象调用一个方法,或者传递一个值 给 生成器,然后通过yield语句返回,或者通过一个不同的方法向生成器注入异常。
第一个新方法是send(value),类似于next(),但是将value传递进作为yield表达式值的生成器中。事实上,g.next()和g.send(None)是等效的。
第二个新方法是throw(type, value=None, traceback=None),等效于在yield语句处
raise type, value, traceback
不像raise(从执行点立即引发异常),throw()首先恢复生成器,然后仅仅引发异常。选用单次throw就是因为它意味着把异常放到其它位置,并且在其它语言中与异常有关。
当生成器中的异常被引发时发生什么?它可以或者显式引发,当执行某些语句时可以通过throw()方法注入到yield语句中。任一情况中,异常都以标准方式传播:它可以被except和finally捕获,或者造成生成器的中止并传递给调用者。
因完整性缘故,值得提及生成器迭代器也有close()方法,该方法被用来让本可以提供更多值的生成器立即中止。它用生成器的__del__方法销毁保留生成器状态的对象。
让我们定义一个只打印出通过send和throw方法所传递东西的生成器。
>>> import itertools >>> def g(): ... print '--start--' ... for i in itertools.count(): ... print '--yielding %i--' % i ... try: ... ans = yield i ... except GeneratorExit: ... print '--closing--' ... raise ... except Exception as e: ... print '--yield raised %r--' % e ... else: ... print '--yield returned %s--' % ans >>> it = g() >>> next(it) --start-- --yielding 0-- 0 >>> it.send(11) --yield returned 11-- --yielding 1-- 1 >>> it.throw(IndexError) --yield raised IndexError()-- --yielding 2-- 2 >>> it.close() --closing--
注意: next还是__next__?
在Python 2.x中,接受下一个值的迭代器方法是next,它通过全局函数next显式调用,意即它应该调用__next__。就像全局函数iter调用__iter__。这种不一致在Python 3.x中被修复,it.next变成了it.__next__。对于其它生成器方法——send和throw情况更加复杂,因为它们不被解释器隐式调用。然而,有建议语法扩展让continue带一个将被传递给循环迭代器中send的参数。如果这个扩展被接受,可能gen.send会变成gen.__send__。最后一个生成器方法close显然被不正确的命名了,因为它已经被隐式调用。
链式生成器
注意: 这是PEP 380的预览(还未被实现,但已经被Python3.3接受)
比如说我们正写一个生成器,我们想要yield一个第二个生成器——一个子生成器(subgenerator)——生成的数。如果仅考虑产生(yield)的值,通过循环可以不费力的完成:
subgen = some_other_generator() for v in subgen: yield v
然而,如果子生成器需要调用send()、throw()和close()和调用者适当交互的情况下,事情就复杂了。yield语句不得不通过类似于前一章节部分定义的try...except...finally结构来保证“调试”生成器函数。这种代码在PEP 380中提供,现在足够拿出将在Python 3.3中引入的新语法了:
yield from some_other_generator()
像上面的显式循环调用一样,重复从some_other_generator中产生值直到没有值可以产生,但是仍然向子生成器转发send、throw和close。
更多Python中的迭代器与生成器高级用法相关文章请关注PHP中文网!