IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Python, de zéro


précédentsommairesuivant

XI. Les générateurs

XI-1. Introduction

Une fonction pouvant renvoyer tout objet Python peut donc parfaitement renvoyer un tuple ou une liste.

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
from datetime import *
import time

def fct(x):
    ret=[]
    for i in range(x):
        ret.append(datetime.today().strftime("%H:%M:%S"))
    return ret

for (i, f) in enumerate(fct(5), 1):
    print(i, f)
    time.sleep(5)
print("Terminé: ", datetime.today().strftime("%H:%M:%S"))

Résultat (qui mettra 25 secondes pour s’afficher) :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
from datetime import *
import time

def fct(x):
    for i in range(x):
        yield datetime.today().strftime("%H:%M:%S")

for (i, g) in enumerate(fct(5), 1):
    print(i, g)
    time.sleep(5)
print("Terminé: ", datetime.today().strftime("%H:%M:%S"))

Résultat (qui mettra aussi 25 secondes pour s’afficher) :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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()).

 
Sélectionnez
1.
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().

 
Sélectionnez
1.
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).

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
2.
3.
4.
5.
>>> tab=genere_info()
>>> print(tuple(tab))
(1, 2, 3, 4, 5)
>>> print(tuple(tab))
()

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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.


précédentsommairesuivant

Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.