Un générateur, c’est une fonction qui produit des valeurs une par une au lieu de tout calculer d’un coup. Résultat concret : tu peux parcourir un fichier de logs de 10 Go avec seulement quelques kilo-octets de RAM. Le mot-clé yield rend ça possible en mettant la fonction en pause entre chaque valeur. Ce guide t’accompagne pas à pas, en partant de ce que tu connais déjà (les listes et les boucles for) jusqu’aux cas réels.
Prérequis : savoir écrire une fonction Python avec return (voir le guide sur les fonctions).
Partons de ce que tu connais : les listes
Section intitulée « Partons de ce que tu connais : les listes »Tu sais déjà créer une liste et boucler dessus :
nombres = [0, 1, 2, 3, 4]
for n in nombres: print(n)La boucle for parcourt la liste élément par élément. Ça fonctionne très bien pour 5 éléments. Mais que se passe-t-il si tu en as un million ?
On peut créer une liste en une seule ligne grâce à une compréhension de liste (list comprehension). La syntaxe [expression for variable in itérable] applique l’expression à chaque élément et range le résultat dans une liste :
# Forme longue (boucle classique)carres = []for x in range(5): carres.append(x ** 2)# carres = [0, 1, 4, 9, 16]
# Forme courte (compréhension de liste) — même résultatcarres = [x ** 2 for x in range(5)]# carres = [0, 1, 4, 9, 16]C’est une écriture compacte très courante en Python (voir le guide sur les listes pour plus de détails). Maintenant, que se passe-t-il si on l’utilise avec un million d’éléments ?
carres = [x ** 2 for x in range(1_000_000)]Python construit toute la liste en mémoire avant que la boucle for ne commence. Mesurons la taille avec sys.getsizeof(), une fonction du module sys qui renvoie la taille en octets d’un objet en mémoire :
import sys
carres_liste = [x ** 2 for x in range(1_000_000)]print(sys.getsizeof(carres_liste))# 8448728 → environ 8 Mo en RAMPour un million d’éléments, 8 Mo ça passe. Mais pour 100 millions ? 800 Mo. Pour un milliard ? 8 Go. Et si tu lis un fichier de logs de 10 Go avec readlines(), c’est 10 Go de RAM consommés d’un coup.
La question est : est-ce qu’on a vraiment besoin de TOUS les éléments en même temps ? En général non : la boucle for ne traite qu’un seul élément à la fois. C’est exactement ce problème que les générateurs résolvent.
Ta première fonction génératrice
Section intitulée « Ta première fonction génératrice »yield : comme return, mais en pause
Section intitulée « yield : comme return, mais en pause »Voici deux fonctions qui font presque la même chose. La première utilise return, la seconde utilise yield :
def carres_liste(n): """Calcule tous les carrés et renvoie une liste.""" resultat = [] for i in range(n): resultat.append(i ** 2) return resultat # Renvoie TOUT d'un coup
# Utilisationfor carre in carres_liste(5): print(carre)# 0, 1, 4, 9, 16def carres_generateur(n): """Produit les carrés un par un.""" for i in range(n): yield i ** 2 # Produit UNE valeur, puis se met en pause
# Utilisation (identique !)for carre in carres_generateur(5): print(carre)# 0, 1, 4, 9, 16Le résultat est le même. Mais la mécanique interne est radicalement différente.
L’analogie du marque-page : imagine que tu lis un livre. Avec return, tu photocopies le livre entier et tu donnes toutes les pages d’un coup. Avec yield, tu lis une page, tu poses un marque-page, tu donnes la page au lecteur, et tu attends qu’il te dise “page suivante”. Quand il le demande, tu reprends la lecture exactement là où tu t’étais arrêté.
La preuve par la mémoire
Section intitulée « La preuve par la mémoire »Mesurons la mémoire occupée par chacun avec sys.getsizeof() (qui renvoie la taille en octets). On affiche le résultat avec une f-string — une chaîne préfixée par f qui permet d’insérer des variables entre accolades {} (voir le guide sur le formatage de chaînes pour approfondir) :
import sys
# La version liste (définie plus haut)resultat_liste = carres_liste(1_000_000)
# La version générateur (définie plus haut)resultat_gen = carres_generateur(1_000_000)
taille_liste = sys.getsizeof(resultat_liste)taille_gen = sys.getsizeof(resultat_gen)
print(f"Liste : {taille_liste} octets") # → 8448728 (~8 Mo)print(f"Générateur : {taille_gen} octets") # → 200 octetsLa liste pèse 8 Mo, le générateur pèse 200 octets. C’est plus de 40 000 fois moins. Et cette taille reste constante, que tu génères 1 000 ou 1 milliard d’éléments.
Comprendre le mécanisme pas à pas
Section intitulée « Comprendre le mécanisme pas à pas »Étape 1 : L’appel ne lance rien
Section intitulée « Étape 1 : L’appel ne lance rien »def compteur(maximum): print("--- DÉMARRAGE ---") n = 0 while n < maximum: print(f" Je vais produire {n}") yield n print(f" Je reprends après {n}") n += 1 print("--- FIN ---")
gen = compteur(3)print(type(gen))# <class 'generator'># (Aucun print de la fonction n'apparaît !)Quand tu appelles compteur(3), Python ne lance pas le code. Il crée un objet générateur — une sorte de télécommande vers la fonction. Le code ne commencera que quand tu demanderas la première valeur.
Étape 2 : next() lance l’exécution jusqu’au prochain yield
Section intitulée « Étape 2 : next() lance l’exécution jusqu’au prochain yield »valeur = next(gen)# --- DÉMARRAGE --- ← le code démarre enfin# Je vais produire 0 ← s'exécute normalement# ← PAUSE sur yieldprint(valeur)# 0 ← la valeur produite par yieldLa fonction s’exécute depuis le début jusqu’au premier yield. Elle renvoie la valeur 0 et se fige. Toutes ses variables (n = 0, maximum = 3) restent intactes en mémoire.
Étape 3 : next() reprend exactement au bon endroit
Section intitulée « Étape 3 : next() reprend exactement au bon endroit »valeur = next(gen)# Je reprends après 0 ← reprend APRÈS le yield précédent# Je vais produire 1 ← continue la boucle# ← PAUSE sur yieldprint(valeur)# 1Étape 4 : Le générateur s’épuise
Section intitulée « Étape 4 : Le générateur s’épuise »next(gen)# Je reprends après 1# Je vais produire 2# → renvoie 2
next(gen)# Je reprends après 2# --- FIN --- ← la boucle while est terminée# StopIteration ← EXCEPTION ! Plus de valeurs.Quand la fonction atteint sa fin (ou un return sans valeur), Python lève StopIteration. C’est le signal qui dit “c’est terminé, il n’y a plus rien à produire”.
La boucle for fait tout ça automatiquement
Section intitulée « La boucle for fait tout ça automatiquement »Tu n’as pas besoin d’appeler next() manuellement. La boucle for le fait et gère StopIteration pour toi :
for valeur in compteur(3): print(f"Reçu : {valeur}")C’est équivalent à ce code (que tu n’as jamais besoin d’écrire) :
gen = compteur(3)while True: try: valeur = next(gen) except StopIteration: break print(f"Reçu : {valeur}")Comparaison return vs yield
Section intitulée « Comparaison return vs yield »| Critère | return | yield |
|---|---|---|
| Ce qui se passe | La fonction s’exécute entièrement, puis renvoie le résultat | La fonction se met en pause à chaque yield et renvoie une valeur |
| Mémoire | Tout le résultat est en mémoire (liste complète) | Une seule valeur à la fois (taille constante) |
| Ce que tu récupères | La valeur directement (liste, dict, string…) | Un objet generator sur lequel tu itères |
| Réutilisable ? | Oui, le résultat est une liste classique | Non — le générateur est à usage unique |
| Quand l’utiliser | Petit résultat, besoin d’index ou de len() | Gros volume, traitement séquentiel, flux infini |
Exemple concret : la suite de Fibonacci
Section intitulée « Exemple concret : la suite de Fibonacci »La suite de Fibonacci (0, 1, 1, 2, 3, 5, 8, 13, …) est un cas d’école parfait : c’est une séquence potentiellement infinie où chaque valeur dépend des précédentes.
def fibonacci_liste(n): """Renvoie les n premiers nombres de Fibonacci.""" fib = [] a, b = 0, 1 for _ in range(n): fib.append(a) a, b = b, a + b return fib # Toute la liste d'un coup
print(fibonacci_liste(10))# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]def fibonacci(): """Produit la suite de Fibonacci à l'infini.""" a, b = 0, 1 while True: # Boucle infinie ! yield a # Produit le nombre courant a, b = b, a + b
# Prendre les 10 premiersfrom itertools import islicefor n in islice(fibonacci(), 10): print(n, end=" ")# 0 1 1 2 3 5 8 13 21 34La version liste doit savoir à l’avance combien d’éléments produire. Le générateur s’en fiche : il produit des valeurs à l’infini, et c’est l’appelant qui décide quand s’arrêter. Et quand il s’arrête, plus aucune mémoire n’est gaspillée.
Les expressions génératrices
Section intitulée « Les expressions génératrices »Tu connais les list comprehensions ([... for ... in ...]). Les expressions génératrices utilisent la même syntaxe, mais avec des parenthèses au lieu de crochets :
# List comprehension → construit toute la liste en mémoirecarres_liste = [x ** 2 for x in range(1_000_000)]
# Generator expression → produit les valeurs une par unecarres_gen = (x ** 2 for x in range(1_000_000))La seule différence visible, c’est [] vs (). Mais côté mémoire, c’est le jour et la nuit (rappelle-toi : 8 Mo vs 200 octets).
Chaîner des expressions génératrices (pipeline)
Section intitulée « Chaîner des expressions génératrices (pipeline) »L’intérêt principal des expressions génératrices, c’est qu’elles se chaînent pour former un pipeline de traitement :
# Pipeline : lire → nettoyer → filtrer → afficherwith open("/var/log/syslog") as f: lignes = (l.strip() for l in f) # 1. Nettoyer erreurs = (l for l in lignes if "ERROR" in l) # 2. Filtrer recentes = (l for l in erreurs if "2026-02" in l) # 3. Affiner
for erreur in recentes: # 4. Traiter print(erreur)Ici, aucune liste intermédiaire n’est créée. Le fichier est lu ligne par ligne, chaque ligne passe dans la chaîne, et seules les lignes qui correspondent sont affichées. Même un fichier de 50 Go est traité sans problème.
yield from : déléguer à un sous-générateur
Section intitulée « yield from : déléguer à un sous-générateur »Quand un générateur doit produire les valeurs d’un autre itérable, tu peux écrire une boucle for + yield. Mais yield from est plus direct :
def tous_les_nombres(): for n in range(3): yield n for n in range(10, 13): yield n
print(list(tous_les_nombres()))# [0, 1, 2, 10, 11, 12]def tous_les_nombres(): yield from range(3) # Délègue à range(3) yield from range(10, 13) # Puis à range(10, 13)
print(list(tous_les_nombres()))# [0, 1, 2, 10, 11, 12]yield from iterable dit : “produis toutes les valeurs de cet itérable, une par une, comme si je les avais yield moi-même”. C’est du sucre syntaxique, mais ça rend le code plus lisible.
Cas pratique : parcourir une arborescence de fichiers
Section intitulée « Cas pratique : parcourir une arborescence de fichiers »from pathlib import Path
def parcourir(dossier): """Parcourt récursivement un dossier et produit chaque fichier.""" chemin = Path(dossier) for item in sorted(chemin.iterdir()): if item.is_file(): yield item elif item.is_dir(): yield from parcourir(item) # Récursion via yield from
# Utilisationfor fichier in parcourir("/etc/nginx"): print(fichier)Ici yield from parcourir(item) délègue la récursion au sous-dossier. Chaque fichier remonte directement à l’appelant, sans liste intermédiaire.
Cas pratiques
Section intitulée « Cas pratiques »Lire un fichier volumineux sans exploser la RAM
Section intitulée « Lire un fichier volumineux sans exploser la RAM »def lire_fichier(chemin, encodage="utf-8"): """Lit un fichier ligne par ligne sans tout charger en mémoire.""" with open(chemin, encoding=encodage) as f: for numero, ligne in enumerate(f, 1): yield numero, ligne.rstrip("\n")
# Chercher les erreurs dans un fichier de 10 Gofor num, ligne in lire_fichier("/var/log/syslog"): if "ERROR" in ligne: print(f"L{num}: {ligne}")Le fichier n’est jamais entièrement chargé. Chaque ligne est lue, traitée, puis oubliée. Tu pourrais scanner un fichier de 100 Go avec 10 Mo de RAM.
Pipeline de traitement de logs
Section intitulée « Pipeline de traitement de logs »Un pipeline de générateurs connecte des étapes de traitement comme des tuyaux :
def lire_lignes(chemin): """Étape 1 : lire le fichier ligne par ligne.""" with open(chemin) as f: yield from (l.rstrip("\n") for l in f)
def filtrer(lignes, mot_cle): """Étape 2 : ne garder que les lignes contenant le mot-clé.""" for ligne in lignes: if mot_cle in ligne: yield ligne
def extraire_ip(lignes): """Étape 3 : extraire l'adresse IP de chaque ligne.""" import re pattern = re.compile(r"\d{1,3}(?:\.\d{1,3}){3}") for ligne in lignes: match = pattern.search(ligne) if match: yield match.group()
# Assembler le pipeline (rien ne s'exécute encore !)lignes = lire_lignes("/var/log/auth.log")echecs = filtrer(lignes, "Failed password")ips = extraire_ip(echecs)
# C'est seulement ici que tout se déclenche :from collections import Countertop_ips = Counter(ips).most_common(10)for ip, count in top_ips: print(f"{ip:>15} : {count} tentatives")Ce pipeline lit le fichier, filtre les erreurs d’authentification, et extrait les IP — tout ça ligne par ligne, sans jamais charger le fichier entier.
Surveiller un fichier en temps réel (tail -f)
Section intitulée « Surveiller un fichier en temps réel (tail -f) »import time
def tail_follow(chemin): """Suit un fichier comme la commande tail -f.""" with open(chemin) as f: f.seek(0, 2) # Se positionner à la fin du fichier while True: ligne = f.readline() if ligne: yield ligne.rstrip("\n") else: time.sleep(0.1) # Attendre de nouvelles lignes
# Surveiller les connexions SSH en temps réelfor ligne in tail_follow("/var/log/auth.log"): if "Accepted" in ligne: print(f"Connexion : {ligne}")Ce générateur ne s’arrête jamais : il attend les nouvelles lignes et les produit au fur et à mesure. C’est un flux infini, et seul un générateur peut le modéliser proprement.
Paginer des résultats
Section intitulée « Paginer des résultats »def paginer(iterable, taille=10): """Découpe un itérable en pages (lots) de N éléments.""" page = [] for item in iterable: page.append(item) if len(page) == taille: yield page page = [] if page: yield page # Dernière page (éventuellement incomplète)
# Déployer des serveurs par lots de 5serveurs = ["web-1", "web-2", "db-1", "db-2", "cache-1", "web-3", "db-3", "cache-2", "monitor-1", "lb-1"]
for i, lot in enumerate(paginer(serveurs, 5), 1): print(f"Lot {i} : {lot}")# Lot 1 : ['web-1', 'web-2', 'db-1', 'db-2', 'cache-1']# Lot 2 : ['web-3', 'db-3', 'cache-2', 'monitor-1', 'lb-1']yield et les context managers
Section intitulée « yield et les context managers »Tu peux utiliser yield pour créer des context managers (les blocs with) grâce au décorateur @contextmanager :
from contextlib import contextmanagerimport time
@contextmanagerdef chronometre(label): """Mesure le temps d'exécution d'un bloc de code.""" debut = time.perf_counter() print(f" {label} — début") yield # ← LE CODE DU BLOC with S'EXÉCUTE ICI duree = time.perf_counter() - debut print(f" {label} — {duree:.3f}s")
# Utilisationwith chronometre("Traitement des logs"): time.sleep(0.5) print(" Analyse en cours...")
# Traitement des logs — début# Analyse en cours...# Traitement des logs — 0.502sLe yield sépare le code d’entrée (avant) du code de sortie (après). Tout ce qui est dans le bloc with s’exécute “à la place” du yield.
yield dans Textual et les frameworks
Section intitulée « yield dans Textual et les frameworks »Si tu as lu le guide sur Textual, tu as vu yield dans la méthode compose() :
from textual.app import App, ComposeResultfrom textual.widgets import Header, Footer, Static
class MonApp(App): def compose(self) -> ComposeResult: yield Header() yield Static("Contenu principal") yield Footer()compose() est un générateur : Textual appelle next() dessus pour récupérer chaque widget un par un, dans l’ordre. Chaque yield correspond à un widget qui apparaît à l’écran. C’est plus lisible qu’un return [Header(), Static(...), Footer()] parce que les widgets sont déclarés dans l’ordre d’apparition, comme un plan de l’interface.
Pour aller plus loin : send(), throw(), close()
Section intitulée « Pour aller plus loin : send(), throw(), close() »send() : envoyer une valeur dans le générateur
Section intitulée « send() : envoyer une valeur dans le générateur »yield peut aussi recevoir une valeur depuis l’extérieur :
def accumulateur(): """Additionne les valeurs envoyées via send().""" total = 0 while True: valeur = yield total # ← produit total ET reçoit la prochaine valeur if valeur is None: break total += valeur
gen = accumulateur()next(gen) # Amorcer le générateur → produit 0gen.send(10) # → 10 (total = 0 + 10)gen.send(25) # → 35 (total = 10 + 25)gen.send(5) # → 40 (total = 35 + 5)Le yield joue un double rôle : il envoie total vers l’extérieur, et reçoit la valeur passée par send().
close() : arrêter proprement un générateur
Section intitulée « close() : arrêter proprement un générateur »def flux_infini(): n = 0 try: while True: yield n n += 1 except GeneratorExit: print("Générateur fermé proprement")
gen = flux_infini()print(next(gen)) # 0print(next(gen)) # 1gen.close() # Générateur fermé proprementclose() lève GeneratorExit à l’intérieur du générateur, ce qui permet d’exécuter du code de nettoyage dans un bloc try/except.
throw() : injecter une exception
Section intitulée « throw() : injecter une exception »def generateur_robust(): while True: try: valeur = yield print(f"Reçu : {valeur}") except ValueError as e: print(f"Erreur gérée : {e}")
gen = generateur_robust()next(gen) # Amorcergen.send("ok") # Reçu : okgen.throw(ValueError, "donnée invalide") # Erreur gérée : donnée invalidegen.send("reprise") # Reçu : repriseBonnes pratiques
Section intitulée « Bonnes pratiques »Quand utiliser un générateur
Section intitulée « Quand utiliser un générateur »| Situation | Générateur ? | Pourquoi |
|---|---|---|
| Fichier volumineux (> 100 Mo) | Oui | Évite l’explosion mémoire |
| Flux de données infini (logs en temps réel) | Oui | Impossible de tout stocker — le flux ne s’arrête jamais |
| Pipeline de transformations (lire → filtrer → transformer) | Oui | Mémoire stable, code lisible et composable |
| Petit résultat (< 1 000 éléments) | Non | return est plus simple et le résultat est réutilisable |
Besoin d’accès par index (resultat[42]) | Non | Un générateur ne supporte pas l’indexation |
Besoin de len() sur le résultat | Non | len() ne fonctionne pas sur un générateur |
Erreurs courantes
Section intitulée « Erreurs courantes »| Erreur | Ce qui se passe | Comment corriger |
|---|---|---|
Le résultat est <generator object ...> au lieu des valeurs | Tu as oublié d’itérer sur le générateur | Utilise une boucle for, ou list(gen) |
| Le générateur est vide au 2e parcours | Un générateur s’épuise après un seul parcours | Recrée le générateur : gen = ma_fonction() |
StopIteration inattendue | next() appelé après épuisement | Utilise une boucle for (elle gère ça automatiquement) |
TypeError: 'generator' object is not subscriptable | Tentative d’indexation gen[0] | Convertis d’abord en liste : list(gen)[0] |
SyntaxError avec yield dans une lambda | yield n’est pas supporté dans les lambdas | Utilise une fonction def classique |
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
| Le générateur ne produit aucune valeur | Oubli d’itérer (boucle for ou next()) | for x in mon_generateur(): ... |
| Le fichier n’est pas lu | Le bloc with est fermé avant l’itération | Itère à l’intérieur du with, pas après |
RuntimeError: generator raised StopIteration | StopIteration levée dans un yield from (Python 3.7+) | Utilise return au lieu de raise StopIteration |
| Performance plus lente qu’une liste | Overhead des appels next() sur un petit volume | Pour les petites données, une liste est plus rapide — c’est normal |
À retenir
Section intitulée « À retenir »yieldmet la fonction en pause et produit une valeur. Au prochainnext(), elle reprend exactement là où elle s’était arrêtée — c’est le mécanisme du marque-page.- Un générateur ne stocke rien en mémoire : il calcule la prochaine valeur à la demande. C’est ce qu’on appelle l’évaluation paresseuse (lazy evaluation).
- Les expressions génératrices
(x for x in ...)sont la version compacte des fonctions génératrices. Elles se chaînent pour former des pipelines sans consommer de mémoire. yield fromdélègue l’itération à un autre itérable ou générateur — idéal pour les parcours récursifs.- Un générateur est à usage unique : après un parcours complet, il est épuisé. Il faut en recréer un.
- En pratique, utilise la boucle
forpour consommer un générateur — elle gèrenext()etStopIterationautomatiquement. @contextmanager+yieldpermet de créer des context managers (with) en quelques lignes.- Textual, asyncio et d’autres frameworks s’appuient sur
yieldpour structurer l’exécution.