Les générateurs sont une fonctionnalité étonnante de Python et constituent un élément essentiel de la compréhension du langage. Une fois que vous les aurez maîtrisés, vous ne pourrez plus vous en passer.
Rappel sur les itérables
Si vous lisez les éléments un par un dans un tableau, cela s’appelle une itération.
lst = [1, 2, 3]
>>> for i in lst :
… print(i)
1
2
3
Si vous utilisez une liste comme point de départ, vous créez la liste, ce qui en fait un itérable. Dans une boucle for, on parcourt ses éléments un par un et on les répète.
lst = [x*x for x in range(3)]
>>> for i in lst :
… print(i)
0
1
4
Chaque fois que nous pouvons utiliser « for… in » … » dans le contexte de quelque chose, c’est un mot itérable : chaînes de caractères, listes, fichiers, listes…
Les itérables sont utiles, car vous pouvez les lire autant de fois que vous le souhaitez, cependant ce n’est pas toujours le meilleur choix car vous devez stocker tous les composants en mémoire.
Générateurs
Si vous êtes familier avec l’article sur les listes de compréhension, vous pouvez également faire des expressions génératrices :
generateur = (x*x for x in range(3))
>>> for i in generateur :
… print(i)
0
1
4
La seule différence avec la version précédente est que nous employons () au lieu de]. Cependant, vous ne pouvez pas lire deux fois générateur puisque le principe des générateurs est de tout créer en un clin d’œil et calcule 0, l’oublie, calcule 1, l’oublie, puis calcule 4. Chaque étape est effectuée une par une.
Le mot yield
yield est un mot qui est utilisé pour remplacer return, à l’exception du fait que nous recevrons un générateur.
>>> def creerGenerateur() :
… mylist = range(3)
… for i in mylist:
… yield i*i
…
>>> generateur = creerGenerateur() # crée un générateur
>>> print(generateur) # generateur est un objet !
< generator object creerGenerateur at 0x2b484b9addc0>
>>> for i in generateur:
… print(i)
0
1
4
C’est un mauvais exemple, mais dans le monde réel, il est utile d’être conscient que la fonction vous donnera une variété de valeurs que vous n’aurez besoin de regarder qu’une seule fois.
Le secret des maîtres zen qui ont acquis la connaissance transcendantale du rendement est de savoir que lorsque vous appelez la fonction, le code de la fonction n’est pas exécuté. Au lieu de cela, la fonction renvoie un objet appelé générateur.
C’est difficile à comprendre Donc, assurez-vous de lire cette section plusieurs fois.
createGenerator() n’exécute pas le code de la fonction createGenerator.
creerGenerator() renvoie un objet qui est un générateur.
En réalité, tant que l ‘on ne touche pas au générateur, il ne se passe rien. Et, dès que nous commençons à itérer sur le générateur, le code de la fonction est exécuté.
La première fois que le code est exécuté, il commence au début du code, puis atteint yield et renvoie la première valeur. Ensuite, pour chaque boucle, le code reprendra là où il en était (oui, Python sauvegarde l’état du code du générateur entre chaque appel) et exécutera le code jusqu’à ce qu’il atteigne yield. Dans notre cas, il va répéter la boucle.
Il continuera à le faire jusqu’à ce que le code ne parvienne plus à atteindre yield, et donc qu’il n’y ait plus de valeur à renvoyer. Ce générateur sera alors considéré comme vide pour le reste de sa vie. Il ne peut pas être « rembobiné ». Un nouveau générateur doit être fabriqué.
La raison pour laquelle le code n’est plus capable de répondre au yield est vos choix : boucle if/else, récursion, boucle…. Il y a de nombreuses façons de respecter le yield. Vous pourriez même rendre le yield infini.
Un exemple concret du yield python
Cependant, le yield n’est pas seulement un moyen de réduire l’utilisation de la mémoire, il vous permet également de dissimuler la complexité d’un algorithme derrière une API d’itération traditionnelle.
S’il y a une fonction dans votre programme qui, juste comme ça . Extrait les mots de plus de 3 caractères de chaque fichier dans le dossier.
Cela pourrait ressembler à ça :
import os
def extraire_mots(dossier):
for fichier in os.listdir(dossier):
with open(os.path.join(dossier, fichier)) as f:
for ligne in f:
for mot in ligne.split():
if len(mot) > 3:
yield mot
C’est un algo qui est totalement caché aux yeux de l’utilisateur. De leur point de vue, il fait simplement ceci :
for mot in extraire_mots(dossier):
print mot
Et pour lui, c’est transparent. En outre, il est en mesure d’utiliser tous les outils que nous utilisons normalement pour les itérables. Toutes les fonctions qui prennent en charge les itérables prennent les résultats de la fonction en tant que paramètre grâce à la puissance du duck typing. Cela constitue une fantastique boîte à outils.
Rendement du contrôleur
>>> class DistributeurDeCapote():
stock = True
def allumer(self):
while self.stock:
yield « capote »
…
Tant qu’il y a des stocks, vous pouvez acheter le nombre de bouchons que vous voulez.
>>> distributeur_en_bas_de_la_rue = DistributeurDeCapote()
>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> print distribuer.next()
capote
>>> print distribuer.next()
capote
>>> print([distribuer.next() for c in range(4)])
[‘capote’, ‘capote’, ‘capote’, ‘capote’]
Quand il n’y en a pas en stock…
>>> distributeur_en_bas_de_la_rue.stock = False
>>> distribuer.next()
Traceback (most recent call last):
File « <ipython-input-22-389e61418395> », line 1, in <module>
distribuer.next()
StopIteration
< type ‘exceptions.StopIteration’>
C’est le cas pour tout générateur qui est neuf :
>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> distribuer.next()
Traceback (most recent call last):
File « <ipython-input-24-389e61418395> », line 1, in <module>
distribuer.next()
StopIteration
Une machine qui ne fonctionne pas n’a jamais rempli le stock ;), il suffit cependant de réapprovisionner le stock :
>>> distributeur_en_bas_de_la_rue.stock = True
>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> for c in distribuer :
… print c
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
…
itertools : le dernier module préféré
Les générateurs sont uniques en ce sens que vous devez les traiter en fonction de leur nature. Vous ne pouvez les lire qu’une seule fois, et vous êtes incapable de prédire leur longueur à l’avance. Itertools est un programme spécialisé dans ce domaine : map zip et slice… Il possède des fonctions qui fonctionnent avec toutes les itérables, ce qui inclut les générateurs.
Rappelez-vous que les chaînes de caractères, les listes, les ensembles et même les fichiers sont également itérables.
Mettre en chaîne deux itérables et prendre les 10 premiers caractères ? C’est facile !
>>> import itertools
>>> d = DistributeurDeCapote().allumer()
>>> generateur = itertools.chain(« 12345 », d)
>>> generateur = itertools.islice(generateur, 0, 10)
>>> for x in generateur:
… print x
…
1
2
3
4
5
capote
capote
capote
capote
capote
Les dessous de l’itération
Sous le capot, tous les itérables emploient un algorithme appelé « itérateur ». Vous pouvez obtenir l’itérateur en appliquant la fonction iter() sur un itérable.
>>> iter([1, 2, 3])
< listiterator object at 0x7f58b9735dd0>
>>> iter((1, 2, 3))
< tupleiterator object at 0x7f58b9735e10>
>>> iter(x*x for x in (1, 2, 3))
< generator object at 0x7f58b9723820>
Les itérateurs sont capables d’utiliser une méthode after() qui renvoie une valeur à chaque demande de la méthode. Lorsqu’il n’y a plus de valeur retournée, ils lèvent l’exception StopIteration :
>>> gen = iter([1, 2, 3])
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
3
>>> gen.next()
Traceback (most recent call last):
File « < stdin> », line 1, in < module>
StopIteration
Note à tous ceux qui croient que j’invente quand je déclare qu’en Python nous utilisons des exceptions pour réguler le flux du programme (sacrilège ! ), c’est la façon de gérer les boucles internes en Python. Pour les boucles, utilisez iter() pour créer un générateur, et assurez-vous ensuite d’attraper une exception et de vous arrêter. Pour chaque boucle for, vous lèverez une exception et vous ne le saurez même pas.
Pour clarifier, ce qui se passe réellement est que iter() appelle la méthode iter() sur l’objet qui est passé comme argument. Cela signifie que vous pouvez créer vos propres itérables.
>>> class MonIterableRienQuaMoi(object):
… def __iter__(self):
… yield ‘Python’
… yield « ça »
… yield ‘déchire’
…
>>> gen = iter(MonIterableRienQuaMoi())
>>> gen.next()
‘Python’
>>> gen.next()
‘ça’
>>> gen.next()
‘déchire’
>>> gen.next()
Traceback (most recent call last):
File « < stdin> », line 1, in < module>
StopIteration
>>> for x in MonIterableRienQuaMoi():
… print x
…
Python
ça
déchire
Les générateurs c’est top !
Ca permet de décomposer des taches compliquées ou chainées en fonctions plus simple. En clair çà permet de faire une enchainement de commandes comme avec le pipe sous unix :
cat file.txt | grep python | wc -l
On peut facilement faire un équivalent avec des générateurs (l’exemple n’est peut être pas le plus parlant, je sais).