Maison >développement back-end >Tutoriel Python >Utilisation avancée des itérateurs et des générateurs en Python
Itérateur
Un itérateur est un objet qui adhère au protocole d'itération - ce qui signifie essentiellement qu'il a une méthode suivante qui, lorsqu'elle est appelée, renvoie la séquence Projet suivant. Lorsqu’il n’y a aucun élément à renvoyer, déclenchez l’exception StopIteration.
Les objets itérations autorisent une boucle. Il conserve l'état (position) d'une seule itération, ou d'un autre point de vue, un objet itération est requis à chaque fois que la séquence est bouclée. Cela signifie que nous pouvons parcourir la même séquence plusieurs fois. Séparer la logique d'itération de la séquence nous donne plus de façons d'itérer.
Appeler la méthode __iter__ d'un conteneur pour créer un objet itérateur est le moyen le plus direct de maîtriser les itérateurs. La fonction iter nous fait gagner quelques frappes.
>>> 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
Lorsqu'il est utilisé dans une boucle, StopIteration est accepté et arrête la boucle. Mais avec une invocation explicite, nous voyons qu'une fois les éléments de l'itérateur épuisés, y accéder lèvera une exception.
Utilisez for...in en boucle et utilisez également la méthode __iter__. Cela nous permet de commencer de manière transparente à parcourir une séquence. Mais si nous avons déjà un itérateur, nous voulons pouvoir les utiliser de la même manière dans une boucle for. Pour y parvenir, en plus de next, l'itérateur dispose également d'une méthode __iter__ pour renvoyer l'itérateur lui-même (self).
La prise en charge des itérateurs en Python est omniprésente : toutes les séquences et les conteneurs non ordonnés de la bibliothèque standard sont pris en charge. Ce concept a également été étendu à d'autres choses : par exemple, l'objet fichier prend en charge l'itération des lignes.
>>> f = open('/etc/fstab') >>> f is f.__iter__() True
le fichier lui-même est un itérateur et sa méthode __iter__ ne crée pas d'objet séparé : seule la lecture séquentielle à un seul thread est autorisée.
Générer une expression
La deuxième façon de créer un objet itérable consiste à utiliser une expression génératrice, la base de la compréhension de liste. Pour plus de clarté, les expressions générées sont toujours placées entre parenthèses ou expressions. Si des parenthèses sont utilisées, un itérateur générateur est créé. Dans le cas des crochets, ce processus est « court-circuité » et nous obtenons une 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]
Dans Python 2.7 et 3.x, la syntaxe des expressions de liste a été étendue aux expressions de dictionnaire et d'ensemble. Un ensemble est créé lorsqu'une expression générée est entourée d'accolades. Un dictionnaire dict est créé lorsqu'une expression contient des paires clé-valeur de la forme clé:valeur :
>>> {i for i in range(3)} set([0, 1, 2]) >>> {i:i**2 for i in range(3)} {0: 0, 1: 1, 2: 4}
Si vous avez la malchance d'être coincé dans une ancienne version de Python , cette syntaxe est un peu mauvaise :
>>> set(i for i in 'abc') set(['a', 'c', 'b']) >>> dict((i, ord(i)) for i in 'abc') {'a': 97, 'c': 99, 'b': 98}
Générer des expressions est assez simple, il va sans dire. Il n'y a qu'un seul problème qui mérite d'être mentionné : la variable d'index (i) fuit dans les versions de Python inférieures à 3.
Générateurs
Les générateurs sont des fonctions qui produisent une liste de résultats plutôt qu'une valeur unique.
La troisième façon de créer un objet itérable est d'appeler une fonction génératrice. Un générateur est une fonction contenant le mot clé rendement. Il est à noter que la simple présence de ce mot-clé change complètement la nature de la fonction : l'instruction rendement n'a pas besoin d'être invoquée ni même accessible. Mais laissez la fonction devenir un générateur. Lorsqu'une fonction est appelée, les instructions qu'elle contient sont exécutées. Et lorsqu'un générateur est appelé, l'exécution s'arrête avant la première instruction qu'il contient. Un appel à un générateur crée un objet générateur attaché au protocole d'itération. Tout comme les fonctions normales, les appels simultanés et récursifs sont autorisés.
Lorsque next est appelé, la fonction s'exécute jusqu'au premier rendement. Chaque fois qu'une instruction rendement est rencontrée, une valeur renvoyée comme suivante est obtenue. Une fois l'instruction rendement exécutée, l'exécution de la fonction est arrêtée.
>>> 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
Parcourons tout l'historique d'un seul appel de fonction de générateur.
>>> 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
Par rapport à l'exécution de f() dans une fonction normale pour exécuter immédiatement print, gen est assigné sans exécuter d'instructions dans le corps de la fonction. Ce n'est que lorsque gen.next() est appelé next que les instructions jusqu'à la première partie rendement sont exécutées. La deuxième instruction s'imprime -- au milieu -- et arrête l'exécution lorsque le deuxième rendement est rencontré. La troisième impression suivante est terminée et jusqu'à la fin de la fonction, car il n'y a pas de rendement, une exception est levée.
Que se passe-t-il lorsque le contrôle revient à l'appelant après que la fonction cède ? L'état de chaque générateur est stocké dans l'objet générateur. À partir de ce point, la fonction génératrice semble s'exécuter dans un thread séparé, mais ce n'est qu'une illusion : l'exécution est strictement monothread, mais l'interpréteur conserve et stocke l'état entre les demandes de valeur suivantes.
Pourquoi les générateurs sont-ils utiles ? Comme souligné dans la section sur les itérateurs, les fonctions génératrices ne sont qu'une autre façon de créer des objets itérables. Tout ce qui peut être complété par l'instruction rendement peut également être complété par la méthode suivante. Cependant, il y a des avantages à utiliser des fonctions permettant à l’interprète de créer comme par magie des itérateurs. Une fonction peut être beaucoup plus courte qu'une définition de classe qui nécessite les méthodes next et __iter__. Plus important encore, les auteurs de générateurs peuvent plus facilement comprendre les instructions localisées dans des variables locales plutôt que de devoir transmettre les propriétés d'instance de l'objet itérateur entre les appels suivants successifs.
还有问题是为何迭代器有用?当一个迭代器用来驱动循环,循环变得简单。迭代器代码初始化状态,决定是否循环结束,并且找到下一个被提取到不同地方的值。这凸显了循环体——最值得关注的部分。除此之外,可以在其它地方重用迭代器代码。
双向通信
每个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中文网!