Générateurs (instruction yield)

Rappelons qu’une fonction est un bloc d’instructions qui a reçu un nom, dont le fonctionnement dépend d’un certain nombre de paramètres (les arguments de la fonction) et qui renvoie un résultat (au moyen de l’instruction return).

def nom_de_la_fonction (arguments ): # le nom de la fonction, et les paramètres d’appels bloc_d’instructions # on parcourt ce bloc

Dans la pratique, on appelle la fonction en lui passant des arguments, et elle nous renvoie l’expression qui suit le premier return rencontré. Le parcours de la fonction est alors terminé (avec retour au programme “appelant”), et il faut procéder à un nouvel appel à cette fonction pour que son contenu soit à nouveau évalué depuis le début.

Plusieurs appels à cette fonction peuvent ainsi produire une séquence de résultats.

On peut cependant imaginer un autre mécanisme. Notre fonction renverrait un résultat, puis resterait en sommeil (dans l’état où elle se trouve alors). Un nouvel appel de la fonction la “réveillerait” là où on l’avait laissée, lui permettant de nous renvoyer un nouveau résultat (et de se mettre, là encore, en attente). Dans ces situations d’attente, la fonction conserverait son “contexte” (notamment la valeur de ses variables locales), pour permettre un redémarrage propre.

Ce mécanisme est exactement celui des générateurs, qui sont des fonctions définies comme on l’a vu jusqu’à présent, à ceci près que l’instruction return est remplacée par yield (mot anglais qui signifie “donner”, “produire”).

Un exemple classique permettra de comprendre ce dont il s’agit.

On sait que la suite de Fibonacci {(F_n)_{n\ge0}} est définie par {F_0=0}, {F_1=1}, et, pour tout {n\ge2}, {F_n=F_{n-1}+F_{n-2}}.

Voici trois fonctions calculant à leur manière les éléments successifs de la suite de Fibonacci (seule la troisième est un générateur, car elle rend un résultat au moyen de yield).

La fonction fibo1 est un peu particulière puisqu’un seul appel à fibo1(n) génère la liste {[F_0,F_1,\ldots,F_n]}. En revanche l’expression fibo2(n) renvoie le seul nombre de Fibonacci {F_n}. Évaluer [fibo2(n) for n in range(0,11)] est donc ici assez maladroit puisque ça provoque des recalculs systématiques.

Les exemples ci-dessus ne permettent pas de distinguer la différence fondamentale de fonctionnement entre fibo3 et les deux autres fonctions. Essayons d’y voir un peu plus clair ! Tout d’abord, rien ne permet de différencier les objets fibo1, fibo2, et fibo3 : ce sont trois objets de type “function”, situés quelque part en mémoire :

La différence essentielle tient au résultat de l’évaluation de fibo1(n) (c’est une liste), fibo2(n) (c’est l’entier {F_n}) et fibo3(n) (c’est un “générateur”). La première chose à bien comprendre est qu’évaluer fibo3(n) ne provoque pas immédiatement le calcul de ces valeurs : on crée seulement un mécanisme, qui va fonctionner à la demande, et qui va fournir, au coup par coup, chacun des entiers {F_0,F_1,\ldots,F_n}.

Plaçons par exemple le générateur fibo3(10) dans la variable f . Pour demander à f de générer (de “yielder” diraient certains) une nouvelle valeur, on évalue next(f )

L’exemple précédent montre que le générateur f =fibo3(10) constitue un mécanisme qui peut être sollicité à tout moment par next(f ). Chaque appel renvoie la valeur qui suit l’instruction yield dans le corps de la fonction fibo3.

On voit clairement que le code de la fonction fibo3 est “suspendu” entre deux appels successifs (avec préservation du contenu des variables locales). D’autre part, le code de la fonction fibo3 montre que l’instruction yield est sollicitée au sein de la boucle for k in range(0,n) (avec n = 10 dans notre exemple). Chaque évaluation de next(f ) avance d’une étape dans cette boucle. Quand cette boucle est terminée, l’évaluation de next(f ) se traduit par une erreur de type StopIretation (et c’est normal car on sort enfin du code de la fonction fibo3, mais pas par un yield).

La fonction next accepte un argument par défaut, qui rattrape StopIteration quand on dépasse le nombre d’itérations :

Ce qui est spécialement intéressant dans le fonctionnement d’un générateur, c’est qu’il permet de former les valeurs successives d’une suite virtuellement infinie. Modifions par exemple la fonction fibo3 en supprimant l’argument n :

L’expression fibo4() crée alors un générateur qui permet de délivrer les valeurs successives de la suite de Fibonacci, ad libitum, et au rythme qu’on veut…

Important : si f est un générateur, on peut en solliciter toutes les valeurs au moyen de boucles “for indice in f” (si l’ensemble des valeurs est fini, c’est préférable). Dans ce cas c’est la boucle qui déclenche elle-même le mécanisme “next” et qui rattrape l’exception de fin d’itération. En reprenant les notations précédentes, voici comment “yielder” les 10 premières valeurs de la suite de Fibonacci.

Autre possibilité, déjà évoquée, avec le constructeur list (qui lui aussi prend en charge le mécanisme d’itération) :

Retour à l’accueil du cours Python/Numpy

Sommaire du chapitre 6

  1. Dictionnaires
  2. Ensembles
  3. Itérateurs
  4. Fonctions utiles sur les itérateurs
  5. Générateurs (instruction yield)
  6. Fichiers