Générateurs¶
Plus d’informations : L’excellent cours d’Antoine Rozo
Fonction génératrice et mot-clé yield
¶
Les générateurs sont une façon plus simple d’implémenter les itérateurs.
Au lieu de créer une classe avec les deux méthodes du protocole
d’itération, on définit une fonction qui retourne les résultats avec le
mot clé yield
. Lorsqu’une fonction possède ce mot-clé, l’appeler
crée un générateur (rien d’autre n’est exécuté). Un générateur est un
itérateur qui possède quelques méthodes supplémentaires (cf. plus bas);
ce sont en quelque sorte des itérateurs upgradés. On appelle par abus de
langage les fonctions qui retournent des générateurs (des fonctions
génératrices ) des générateurs aussi (alors que ce sont de simples
fonctions).
Pour faire le lien avec les itérateurs, on peut réécrire l’exemple précédent à l’aide d’un générateur.
def incrementor(max):
n = 0
while n <= max:
yield n
n += 1
On utilise les générateurs comme des itérateurs (les générateurs sont
des itérateurs). Lorsque l’on évalue la méthode __next__()
sur un
générateur (en faisant next(generateur)
), celui-ci parcourt la
fonction génératrice jusqu’au premier yield
qu’il rencontre, puis
s’arrête. Lorsque la méthode __next__()
est de nouveau appelée, le
générateur continue le parcours jusqu’au yield
suivant, et ainsi de
suite. Lorsqu’il n’y en a plus, le générateur lève une exception
StopIteration
.
>>> gen = incrementor(2)
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen) # Erreur
StopIteration
>>> for i in incrementor(2):
... print(i)
0
1
2
On n’appelle pas la méthode iter()
sur un générateur. Ainsi, pour
réinitialiser le générateur, on doit le recréer en appelant une nouvelle
fois la fonction génératrice.
Remarque : Distinction entre fonction génératrice et générateur
>>> incrementor
<function incrementor at 0x00000240AA034D08> # simple fonction
>>> incrementor(1)
<generator object incrementor at 0x00000240A9FC74F8> # générateur
Fonctions supplémentaires¶
En plus du mot-clé yield
, on peut utiliser des fonctions
supplémentaires dans les générateurs:
generateur.send()
Cette méthode permet de communiquer avec le générateur en lui envoyant une valeur. Lorsqu’elle est appelée avec un argument, celui-ci est envoyé au
yield
actuellement atteint, et le générateur reprend le parcours jusqu’auyield
suivant. Ainsi, appeler cette méthode consomme une itération ! Lorsqu’on utilise cette méthode, il ne faut pas oublier d’affecteryield
à une variable, sinon l’argument donné parsend
sera perdu. Séquentiellement, cela donne:
Le générateur vient d’être créé. On appelle
__next__()
, le générateur parcourt la fonction jusqu’au premieryield
.Le générateur rencontre un
yield
. Il envoie ce que leyield
lui fournit et se met en pause.Le générateur est à nouveau appelé. Si c’est avec un
send()
, il passe son argument auyield
qui la donne à la variable à laquelle il est affecté.Une fois que c’est fait, le générateur reprend le parcours de la fonction jusqu’au
yield
suivant, (retour à l’étape 2).
Exemple : On reprend l’incrémenteur et on veut pourvoir l’étendre,
c’est à dire lui envoyer un nombre et l’ajouter au maximum initial. Pour
cela, on modifie la fonction génératrice et on ajoute des print()
pour voir comment fonctionne send()
(on utilise les f-strings).
def incrementor(max):
n = 0
while n <= max:
print(f"max_pre : {max}")
add_max = yield n
print(f"max_post : {max}")
print(f"add_max : {add_max}")
n += 1
max = max + add_max if add_max else max
Lorsque l’on appelle send
, la valeur est stockée dans add_max
.
On peut alors étendre l’incrémenteur.
>>> gen = incrementor(2)
>>> next(gen) # Le générateur est appelé, il commence le parcours
max_pre : 2 # Il rencontre le premier print()
0 # et un yield : il envoie ce que celui-ci lui fournit...
>>> next(gen) # et attend qu'on le rappelle !
max_post : 2 # il reprend son parcours
add_max : None # on voit bien que l'on a rien envoyé au générateur
max_pre : 2
1
>>> next(gen)
max_post : 2
add_max : None # toujours rien...
max_pre : 2
2
>>> gen.send(3) # Le yield retourne à add_max la valeur de send()
max_post : 2 # le générateur reprend le parcours...
add_max : 3 # On a bien un add_max de 3 !
max_pre : 5 # arrivé au while, le max a donc changé, la boucle peut donc continuer !
3 # et on atteint bien le yield suivant
>>> next(gen)
max_post : 5
add_max : None
max_pre : 5
4
En fait, on s’aperçoit que next(gen)
et gen.send(None)
sont
équivalents. On ne peut pas appeler send()
avec autre chose que
None
en paramètre avant d’avoir appelé au moins une fois
__next__()
. En effet, l’affectation se fait par l’intermédiaire du
yield
actuel.
generateur.throw(type[, value, traceback])
Envoie une exception au générateur. Si celui-ci l’attrape, alors retourne également la valeur suivante du générateur.
generateur.close()
Envoie une exception au générateur