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. .. code:: python3 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``. .. code:: pycon >>> 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 .. code:: python3 >>> incrementor # simple fonction >>> incrementor(1) # 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’au ``yield`` suivant. Ainsi, appeler cette méthode consomme une itération ! Lorsqu’on utilise cette méthode, il ne faut pas oublier d’affecter ``yield`` à une variable, sinon l’argument donné par ``send`` 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 premier ``yield``. #. Le générateur rencontre un ``yield``. Il envoie ce que le ``yield`` lui fournit et se met en pause. #. Le générateur est à nouveau appelé. Si c’est avec un ``send()``, il passe son argument au ``yield`` 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). .. code:: python3 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. .. code:: pycon >>> 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