Remarque : ce billet suppose une syntaxe Python 3.x. †
A générateur est simplement une fonction qui renvoie un objet sur lequel vous pouvez appeler next
de telle sorte que, à chaque appel, il renvoie une valeur, jusqu'à ce qu'il déclenche un message d'erreur. StopIteration
exception, signalant que toutes les valeurs ont été générées. Un tel objet est appelé un itérateur .
Les fonctions normales renvoient une valeur unique en utilisant return
tout comme en Java. En Python, cependant, il existe une alternative, appelée yield
. Utilisation de yield
n'importe où dans une fonction en fait un générateur. Observez ce code :
>>> def myGen(n):
... yield n
... yield n + 1
...
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Comme vous pouvez le voir, myGen(n)
est une fonction qui donne n
y n + 1
. Chaque appel à next
produit une seule valeur, jusqu'à ce que toutes les valeurs aient été produites. for
appel de boucles next
en arrière-plan, donc :
>>> for n in myGen(6):
... print(n)
...
6
7
De même, il existe expressions de générateur qui permettent de décrire succinctement certains types courants de générateurs :
>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Notez que les expressions de générateur ressemblent beaucoup à compréhensions de listes :
>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
Observez qu'un objet générateur est généré une fois mais son code est no s'exécuter en une seule fois. Seuls les appels à next
exécuter effectivement (une partie) du code. L'exécution du code dans un générateur s'arrête une fois qu'un code yield
a été atteint, après quoi il renvoie une valeur. L'appel suivant à next
puis fait en sorte que l'exécution se poursuive dans l'état dans lequel le générateur a été laissé après la dernière yield
. Il s'agit d'une différence fondamentale avec les fonctions ordinaires : celles-ci commencent toujours l'exécution au "début" et abandonnent leur état lorsqu'elles renvoient une valeur.
Il y a d'autres choses à dire sur ce sujet. Il est par exemple possible de send
dans un générateur ( référence ). Mais je vous suggère de ne pas vous pencher sur cette question avant d'avoir compris le concept de base d'un générateur.
Vous vous demandez peut-être : pourquoi utiliser des générateurs ? Il y a plusieurs bonnes raisons :
- Certains concepts peuvent être décrits de manière beaucoup plus succincte à l'aide de générateurs.
- Au lieu de créer une fonction qui renvoie une liste de valeurs, on peut écrire un générateur qui génère les valeurs à la volée. Cela signifie qu'il n'est pas nécessaire de construire une liste, ce qui signifie que le code résultant est plus efficace en termes de mémoire. De cette manière, il est même possible de décrire des flux de données qui seraient tout simplement trop volumineux pour tenir en mémoire.
-
Les générateurs permettent de décrire de manière naturelle infini les flux. Considérons par exemple le Les nombres de Fibonacci :
>>> def fib():
... a, b = 0, 1
... while True:
... yield a
... a, b = b, a + b
...
>>> import itertools
>>> list(itertools.islice(fib(), 10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Ce code utilise itertools.islice
pour prendre un nombre fini d'éléments dans un flux infini. Nous vous conseillons de jeter un coup d'œil aux fonctions de l'application itertools
car ce sont des outils essentiels pour écrire des générateurs avancés avec une grande facilité.
† À propos de Python <=2.6 : dans les exemples ci-dessus next
est une fonction qui appelle la méthode __next__
sur l'objet donné. Dans Python <=2.6 on utilise une technique légèrement différente, à savoir o.next()
au lieu de next(o)
. Python 2.7 a next()
appelez .next
il n'est donc pas nécessaire d'utiliser ce qui suit en 2.7 :
>>> g = (n for n in range(3, 5))
>>> g.next()
3