Menu
Français

Générateurs Python : yield, itération paresseuse et expressions génératrices

Comment les générateurs produisent des valeurs paresseusement en Python — le mot-clé yield, les expressions génératrices, et quand ils battent une simple liste.

Une fonction qui marque une pause

Un générateur ressemble à une fonction normale, mais au lieu de calculer tout un résultat et de le renvoyer, il produit une valeur à la fois, en marquant une pause entre les productions jusqu'à ce que quelqu'un veuille la valeur suivante.

Le plus simple possible :

main.py
Output
Click Run to see the output here.

Remarque yield au lieu de return. La première fois que for demande une valeur, Python exécute le corps de la fonction jusqu'à ce qu'il rencontre yield 1. La fonction se met en pause à cet endroit, rend 1 à la boucle, et se souvient exactement où elle s'est arrêtée — variables comprises. L'itération suivante reprend là où elle s'était arrêtée : current += 1, retour au while, yield 2. Et ainsi de suite jusqu'à ce que la condition de la boucle échoue, moment où le générateur s'arrête simplement.

Cette pause-et-reprise, c'est toute l'astuce.

Pourquoi ne pas juste construire une liste ?

Parce que la version liste alloue toutes les valeurs à l'avance :

main.py
Output
Click Run to see the output here.

Bien pour 5 éléments. Maintenant imagine que tu veux 50 millions d'entiers, et tu ne te soucies que du premier qui correspond à une condition. La version liste alloue 50 millions d'entiers et ensuite tu en jettes la plupart. La version générateur crée exactement autant que l'appelant en consomme. Quand la boucle for trouve ce qu'elle veut et fait break, le générateur s'arrête simplement.

C'est le motif à intérioriser : les générateurs te permettent d'écrire du code d'itération sans décider à l'avance combien du résultat tu auras besoin.

Expressions génératrices

Si tu as écrit une compréhension de liste, tu connais déjà la syntaxe — échange les crochets pour des parenthèses :

main.py
Output
Click Run to see the output here.

squares_gen ne calcule encore rien. C'est juste une recette. L'itérer lance la recette une étape à la fois.

Les expressions génératrices sont parfaites comme arguments de fonctions qui consomment un itérable :

main.py
Output
Click Run to see the output here.

Pas de liste intermédiaire. sum, max et any lisent les valeurs une à la fois, ce qui est exactement ce qu'ils veulent.

Lire un gros fichier, ligne par ligne

C'est le cas canonique du monde réel pour les générateurs — traiter un fichier trop gros pour charger en mémoire :

def parse_log_lines(path):
    with open(path) as f:
        for line in f:
            if line.startswith("ERROR"):
                yield line.rstrip()

for error in parse_log_lines("app.log"):
    print(error)

Le fichier est lu paresseusement. Chaque appel au générateur tire une ligne du disque, la filtre et la produit. L'usage mémoire reste plat peu importe la taille du fichier.

Une fois et c'est tout

Un générateur a un seul passage. Après avoir itéré jusqu'à la fin, il est épuisé :

main.py
Output
Click Run to see the output here.

La seconde boucle n'affiche rien. Le générateur n'a plus rien.

Si tu as besoin d'itérer plus d'une fois, soit rappelle la fonction génératrice pour un générateur frais, soit matérialise la séquence avec list(...) et itère la liste plusieurs fois. Choisis selon le coût : reconstruire va bien si le travail est bon marché ; une liste va bien si la séquence est petite.

next() et itération manuelle

Tu n'es pas obligé d'utiliser une boucle for. next() tire une valeur à la fois :

main.py
Output
Click Run to see the output here.

StopIteration est comment un générateur signale « j'ai fini ». Les boucles for l'attrapent silencieusement. Dans du code manuel, tu peux passer un défaut à next(gen, default) pour éviter l'exception.

Générateurs infinis

Parce que les valeurs sont produites à la demande, un générateur peut représenter une séquence sans fin — tant que le consommateur arrête de demander :

main.py
Output
Click Run to see the output here.

while True avec un yield à l'intérieur ne bloque pas le programme — ça veut juste dire « si quelqu'un continue de demander, continue de produire ». Le consommateur décide quand arrêter.

Ce motif apparaît dans les données en streaming, les event loops et partout où tu tires des valeurs d'une source qui n'a pas de longueur définie.

yield from : déléguer à un autre itérable

Si ton générateur veut produire chaque valeur d'un autre itérable, yield from le fait en une ligne :

main.py
Output
Click Run to see the output here.

Sans yield from, tu écrirais une boucle for imbriquée avec yield x à l'intérieur. Ça transfère aussi les appels send() et throw() correctement si tu en utilises un jour — mais pour du code quotidien, pense-y comme « produis chaque valeur de cette chose ».

Quand utiliser un générateur

Trois signaux qu'un générateur est le bon outil :

  1. La séquence est grande, peut-être infinie, ou coûteuse à produire en entier.
  2. Le consommateur pourrait s'arrêter avant la fin (un break sur le premier match, par exemple).
  3. Tu veux enchaîner des transformations — filter, map, take — sans construire de listes intermédiaires.

Et quand ne pas :

  • Tu as besoin d'accès aléatoire (seq[42]). Les générateurs n'avancent que dans un sens.
  • Tu as besoin d'itérer la même séquence plusieurs fois. Utilise une liste.
  • La séquence est petite et tu l'as déjà. Une compréhension de liste est plus simple.

Générateurs, compréhensions de liste et listes simples sont chacun la bonne réponse pour des boulots différents. Le savoir-faire, c'est en choisir un sans trop y penser — et la façon la plus rapide de développer cet instinct, c'est de remarquer, pour chaque itération que tu écris, si « produire tout d'abord » ou « produire un à la fois » convient mieux.

Ensuite : les gestionnaires de contexte en profondeur

Tu as maintenant vu la plupart des idiomes d'itération de Python. Les gestionnaires de contexte — l'instruction with — sont ensuite, et ils s'associent bien avec les générateurs pour streamer des données depuis fichiers et connexions réseau.

Questions fréquentes

Qu'est-ce qu'un générateur en Python ?

Un générateur est une fonction qui produit des valeurs une à la fois, en faisant une pause entre elles. Tu l'écris avec def comme une fonction normale, mais tu utilises yield au lieu de return. L'appeler renvoie un objet générateur ; chaque itération de for ou chaque appel à next() exécute la fonction jusqu'au yield suivant.

Quelle est la différence entre une liste et un générateur ?

Une liste garde chaque élément en mémoire en même temps. Un générateur calcule les éléments à la demande et les oublie après leur consommation. Pour des séquences grandes ou infinies, les générateurs utilisent une toute petite quantité de mémoire fixe ; pour de petits résultats dont tu as besoin plusieurs fois, une liste est meilleure.

Puis-je itérer un générateur deux fois ?

Non. Un générateur est épuisé après le premier passage complet — une seconde boucle for dessus ne produit rien. Si tu as besoin d'itérer plus d'une fois, appelle à nouveau la fonction génératrice pour obtenir un générateur frais, ou matérialise les résultats dans une liste.

Apprendre à coder avec Coddy

COMMENCER