XI. Les générateurs▲
XI-1. Introduction▲
Une fonction pouvant renvoyer tout objet Python peut donc parfaitement renvoyer un tuple ou une liste.
2.
def
genere_info
(
):
return
(
1
, 2
, 3
, 4
, 5
)
Toutefois, les valeurs du tuple ou de la liste renvoyés sont figées dans un état immuable. C’est-à-dire que (par exemple) la valeur « 5 » retournée ici continuera à valoir toujours 5, même si elle est traitée bien longtemps après avoir été retournée…
2.
3.
for
i in
genere_info
(
):
print
(
i)
… # Gros traitement de 45 minutes
Lorsque ce sera au tour de la valeur « 5 » d’être traitée, il se sera écoulé 3h depuis sa création.
Ce comportement peut devenir un souci lorsque la pertinence de la valeur dépend du moment non pas de sa création, mais de son utilisation (contenu d’un stock, horodatage, etc.). Dans ce cas, on ne peut plus passer par le fonctionnement habituel du return
et il est alors nécessaire de passer par un nouvel outil : le générateur.
Un générateur est une fonction qui génère une information non pas au moment où elle est créée, mais au moment où cette information est demandée. Il se construit en utilisant l’instruction yield
.
L’instruction yield
se comporte comme un return
; à ceci près que la valeur n’est pas renvoyée à l’appel de la fonction. Elle est seulement mise en préparation. Le renvoi ne sera effectif (et calculé) qu’au moment réel où l'appelant la demandera.
2.
3.
4.
5.
6.
7.
8.
9.
10.
def
genere_info
(
):
yield
1
yield
2
yield
3
yield
4
yield
5
for
i in
genere_info
(
):
print
(
i)
… # Gros traitement de 45 minutes
À chaque itération, la valeur sera alors effectivement récupérée depuis le générateur au moment où l’itération en cours correspond au yield
prévu à cette itération.
XI-2. Exemple▲
Dans les exemples précédents, il n’y a bien évidemment aucune différence concrète entre les deux traitements (dans tous les cas les valeurs sont figées). Cette différence prendra un sens lorsque les valeurs renvoyées seront calculées et que le moment du calcul aura un impact dans la pertinence du résultat.
Dans les deux exemples ci‑dessous qui utilisent tous deux l’heure courante, on admet qu’ils sont lancés tous les deux à 12h précise…
Sans générateur :
Résultat (qui mettra 25 secondes pour s’afficher) :
2.
3.
4.
5.
6.
1
12
:00
:00
2
12
:00
:00
3
12
:00
:00
4
12
:00
:00
5
12
:00
:00
Terminé: 12
:00
:25
Avec générateur :
Résultat (qui mettra aussi 25 secondes pour s’afficher) :
2.
3.
4.
5.
6.
1
12
:00
:00
2
12
:00
:05
3
12
:00
:10
4
12
:00
:15
5
12
:00
:20
Terminé: 12
:00
:25
Le générateur renvoie l’heure réelle au moment où celle-ci est demandée et non pas au moment où elle est créée.
XI-3. Différentes actions▲
Un générateur peut n’être pas forcément traité dans une itération. Le programmeur peut avoir besoin de récupérer à la volée et de façon impromptue une information générée.
Il peut le faire au travers de la fonction next
(
) appliquée au générateur.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
def
genere_info
(
n):
for
i in
range(
n): yield
i
>>>
g=
genere_info
(
5
)
>>>
next
(
g)
0
>>>
next
(
g)
1
>>>
next
(
g)
2
>>>
next
(
g)
3
Sous Python 2, la fonction next
(
) existe aussi, à la fois sous forme de fonction et sous forme de méthode intégrée au générateur.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
# Exemple Python 2
def
genere_info
(
n):
for
i in
range(
n): yield
i
>>>
g=
genere_info
(
5
)
>>>
next
(
g)
0
>>>
g.next
(
)
1
>>>
next
(
g)
2
>>>
g.next
(
)
3
Un générateur peut aussi recevoir une valeur lors de son travail.
Cela se fait au travers de la méthode send
(
) du générateur. Cette méthode est alors directement reliée à l’instruction interne yield
qui récupère alors la valeur envoyée.
Elle ne peut être invoquée que si le générateur a déjà généré sa première information. Ou alors, elle doit être invoquée avec la valeur None
la première fois.
Et son invocation provoque la génération d’une information (tout comme un appel à la fonction next
(
)).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
def
genere_info
(
n):
for
i in
range(
n):
x=
yield
i
if
x is
not
None
: print
(
"x=[
%s
]"
%
x)
>>>
g=
genere_info
(
5
)
>>>
next
(
g)
0
>>>
next
(
g)
1
>>>
g.send
(
100
)
2
x=
[100
]
Un générateur peut être fermé depuis l’extérieur avant sa fin naturelle au travers de la méthode close
(
).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
genere_info
(
n):
for
i in
range(
n): yield
i
>>>
g=
genere_info
(
5
)
>>>
for
i in
g:
... print
(
i)
... if
i ==
2
: g.close
(
)
...
0
1
2
Un générateur peut aussi être fermé par lui‑même avant sa fin naturelle via l’instruction return
qui ne doit comporter aucune valeur (son but n’est pas de renvoyer une valeur, mais de quitter le générateur).
2.
3.
4.
5.
6.
7.
8.
9.
def
genere_info
(
n):
for
i in
range(
n):
yield
i
if
i ==
2
: return
>>>
for
i in
genere_info
(
5
): print
(
i)
0
1
2
XI-4. Quelques précautions▲
Le générateur possède tout de même un inconvénient : il ne peut être consommé qu’une seule fois.
Cet inconvénient n'est cependant pas aussi réducteur qu'on pourrait le croire. En effet, en général, l’utilisation courante montre que le générateur n'est réellement utilisé qu'une seule fois. Et dans les rares cas où le résultat du générateur doit-être récupéré dans son ensemble (parce qu’utilisé plusieurs fois), on peut alors tout à fait le stocker dans un tuple ou une liste lors de sa création.
2.
3.
4.
5.
>>>
tab=
tuple(
genere_info
(
))
>>>
print
(
tab)
(
1
, 2
, 3
, 4
, 5
)
>>>
print
(
tab)
(
1
, 2
, 3
, 4
, 5
)
Dans ce cas, on reproduit alors le comportement standard d’une fonction qui renvoie un tuple avec ses valeurs définitivement figées.
XI-5. Générateur d’un itérable▲
Le générateur d’un itérable se traduira de façon classique par une boucle sur l’itérable avec chaque élément renvoyé par yield
.
2.
3.
4.
5.
6.
7.
8.
9.
def
genere_info
(
):
for
i in
range(
5
): yield
i
>>>
for
i in
genere_info
(
): print
(
i)
0
1
2
3
4
Python 3 offre un raccourci syntaxique permettant de simplifier ce code en remplaçant la boucle par un générateur directement placé sur l’itérable. Cela se fait avec l’instruction yield
from
.
2.
3.
4.
5.
6.
7.
8.
9.
def
genere_info
(
):
yield
from
range(
5
)
>>>
for
i in
genere_info
(
): print
(
i)
0
1
2
3
4
À noter que cette instruction n’est pas disponible sous Python 2.