dernière modification : 2024

Générateurs python

Les générateurs Python sont une façon d’implémenter des traitements asynchrones.

1. Un exemple de producteur/consommateur

Soit un programme de traitement de données composé de 2 processus indépendants :

  • un processus (coûteux) de génération de données (le producteur),

  • un processus de traitement unitaire de chacune de ces données (le consommateur),

chacun de ces 2 processus étant implémenté sous forme d’une fonction. Un codage naïf du producteur pourrait être :

le producteur
def producteur():
    result = []
    for i in range(10000):
        result.append(gros_calcul_couteux(i))
    return result

le consommateur n’ayant plus qu’à récupérer le retour du producteur pour le traiter :

le consommateur
def consommateur():
  for e in producteur():
    traiter(e)

L’inconvénient de cette solution, c’est que la génération des données doit être complète avant de pouvoir commencer le traitement, donc si pour une raison quelconque, à un moment donné, le consommateur échoue et plante le programme, on aura générer toutes les données pour rien. Un autre inconvénient est que toutes les données doivent tenir en mémoire.

2. yield

Pour éviter ce gâchis, on va modifier le producteur :

def producteur():
    for i in range(10000):
        yield gros_calcul_couteux(i)

La présence de l’instruction yield dans le bloc de code fait passer producteur du statut de fonction au statut de coroutine. Ce changement n’a aucun impact sur le consommateur, qui reste identique. Pourtant, le comportement du programme en est fondamentalement impacté : producteur et consommateur vont se synchronisaer et leur exécution s’entrelacer, le consommateur demandant les données une par une au producteur, et le producteur calculant chaque donnée au moment où on les lui demande.

3. Fonctionnement

Un sous-programme utilisant l’instruction yield s’appelle une fonction génératrice. Son comportement est très différent de celui d’une fonction habituelle. Lorsqu’on l’appelle, elle ne s’exécute pas, mais renvoie simplement un itérateur. C’est cet itérateur, appelé générateur, qui va piloter la fonction génératrice. L’ensemble {fonction génératrice,itérateur} est une façon d’implémenter le concept de coroutine.

Un premier next() sur l’itérateur va exécuter les premières instructions de la fonction génératrice, jusqu’à rencontrer yield. La valeur spécifiée par yield est renvoyée à l’appelant, et le contrôle est redonné à l’appelant. Mais contrairement à return, yield ne termine pas la fonction, il la suspend.

Le next() suivant reprendra l’exécution à l’endroit où elle a été interrompue, jusqu’à rencontrer à nouveau yield, ou la fin de la fonction. Conséquence : les valeurs sont renvoyées au coup par coup, à chaque appel, sans stockage ni anticipation.

Exemple :

def semaine():
    print("début_de_semaine", end=" ")
    for j in ("lu","ma","me","je","ve"):
        yield j
    print("fin_de_semaine")

semaine() est une fonction génératrice. Elle ne s’exécute donc pas lorsqu’on l’appelle :

semaine()  # rien n'est écrit

Elle renvoie simplement un itérateur :

s = semaine()
print(s)         # <generator object semaine at 0x7fc99d33c410>

L’itérateur va, à chaque next(), exécuter la fonction jusqu’au prochain yield :

s = semaine()
print(next(s))    # début_de_semaine lu
print(next(s))    # ma
print(next(s))    # me
print(next(s))    # je
print(next(s))    # ve
print(next(s))    # fin_de_semaine - StopIteration

On peut donc écrire:

for jour in semaine():
  print(jour, end=" ")

pour obtenir :

début_de_semaine lu ma me je ve fin_de_semaine

L’itération se termine lorsque l’itérateur n’a plus rien à renvoyer, lorsqu’il exécute un return, ou lorsqu’il lance une StopIteration. L’itérateur ne sert qu’une seule fois, il faut ré-exécuter la fonction génératrice pour en obtenir un nouveau. On peut donc voir un générateur comme une fonction produisant une succession de résultats à la demande, et se mettant en pause entre chaque demande.

Le générateur est une façon de faire du multi-tâche coopératif : tant qu’il n’exécute pas yield, il conserve la main.