🔝 Retour au Sommaire
Les générateurs sont l'une des fonctionnalités les plus puissantes et élégantes de Python. Ils permettent de créer des itérateurs de manière simple et efficace, en économisant de la mémoire.
Dans ce chapitre, nous allons découvrir comment créer et utiliser des générateurs, comprendre leurs avantages, et explorer les expressions génératrices. Ces concepts sont essentiels pour écrire du code Python performant et élégant.
Un générateur est une fonction spéciale qui produit une séquence de valeurs au lieu de retourner une seule valeur. Contrairement à une fonction normale qui utilise return, un générateur utilise le mot-clé yield.
Fonction normale :
def creer_liste_nombres(n):
"""Crée une liste de nombres de 0 à n-1."""
liste = []
for i in range(n):
liste.append(i)
return liste
nombres = creer_liste_nombres(5)
print(nombres) # [0, 1, 2, 3, 4]
print(type(nombres)) # <class 'list'> Générateur :
def generer_nombres(n):
"""Génère des nombres de 0 à n-1."""
for i in range(n):
yield i
nombres = generer_nombres(5)
print(nombres) # <generator object generer_nombres at 0x...>
print(type(nombres)) # <class 'generator'>
# Pour voir les valeurs, il faut itérer
for nombre in nombres:
print(nombre) # 0, 1, 2, 3, 4Le mot-clé yield :
- Produit une valeur
- Suspend l'exécution de la fonction
- Reprend là où elle s'était arrêtée lors du prochain appel
def mon_generateur():
print("Première valeur")
yield 1
print("Deuxième valeur")
yield 2
print("Troisième valeur")
yield 3
print("Fin")
# Créer le générateur
gen = mon_generateur()
# Récupérer les valeurs une par une
print(next(gen))
# Affiche : Première valeur
# Affiche : 1
print(next(gen))
# Affiche : Deuxième valeur
# Affiche : 2
print(next(gen))
# Affiche : Troisième valeur
# Affiche : 3
# print(next(gen))
# Affiche : Fin
# Lève : StopIterationdef generer_carres(n):
"""Génère les carrés des nombres de 0 à n-1."""
for i in range(n):
yield i ** 2
# Utilisation
for carre in generer_carres(5):
print(carre, end=" ") # 0 1 4 9 16def generer_pairs(debut, fin):
"""Génère tous les nombres pairs entre debut et fin."""
for nombre in range(debut, fin + 1):
if nombre % 2 == 0:
yield nombre
# Utilisation
pairs = generer_pairs(1, 10)
print(list(pairs)) # [2, 4, 6, 8, 10] def fibonacci(n):
"""Génère les n premiers nombres de Fibonacci."""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
# Utilisation
print("Suite de Fibonacci (10 premiers) :")
for nombre in fibonacci(10):
print(nombre, end=" ") # 0 1 1 2 3 5 8 13 21 34Avec une liste (tout en mémoire) :
def creer_grands_nombres():
"""Crée une liste de 1 million de nombres."""
return [i for i in range(1000000)]
# Crée une liste de 1 million d'éléments en mémoire
liste = creer_grands_nombres()
print(f"Taille en mémoire : ~{liste.__sizeof__()} bytes")
# Taille en mémoire : ~8000000 bytes (environ 8 MB)Avec un générateur (valeurs à la demande) :
def generer_grands_nombres():
"""Génère 1 million de nombres à la demande."""
for i in range(1000000):
yield i
# Ne crée qu'un objet générateur, pas les valeurs
gen = generer_grands_nombres()
print(f"Taille en mémoire : ~{gen.__sizeof__()} bytes")
# Taille en mémoire : ~200 bytesLes valeurs sont produites uniquement quand nécessaire :
def generer_avec_traitement(n):
"""Génère des nombres avec un traitement coûteux."""
for i in range(n):
print(f" Traitement de {i}...")
yield i * 2
# Le générateur est créé, mais aucun traitement n'est effectué
gen = generer_avec_traitement(5)
print("Générateur créé")
# Les traitements n'ont lieu que lors de l'itération
print("\nItération :")
for valeur in gen:
print(f"Reçu : {valeur}")
if valeur >= 4: # On peut s'arrêter tôt
break
# Affiche :
# Générateur créé
#
# Itération :
# Traitement de 0...
# Reçu : 0
# Traitement de 1...
# Reçu : 2
# Traitement de 2...
# Reçu : 4Les générateurs peuvent produire des séquences infinies sans problème de mémoire :
def compteur_infini(debut=0):
"""Génère une séquence infinie de nombres."""
nombre = debut
while True:
yield nombre
nombre += 1
# Utilisation avec limitation
compteur = compteur_infini(10)
for i, valeur in enumerate(compteur):
print(valeur, end=" ")
if i >= 9: # Afficher seulement 10 valeurs
break
# Affiche : 10 11 12 13 14 15 16 17 18 19Une expression génératrice est une syntaxe compacte pour créer un générateur, similaire aux compréhensions de listes mais avec des parenthèses.
# Compréhension de liste (crée une liste)
liste = [x ** 2 for x in range(5)]
# Expression génératrice (crée un générateur)
generateur = (x ** 2 for x in range(5))# Compréhension de liste
carres_liste = [x ** 2 for x in range(5)]
print(carres_liste) # [0, 1, 4, 9, 16]
print(type(carres_liste)) # <class 'list'>
# Expression génératrice
carres_gen = (x ** 2 for x in range(5))
print(carres_gen) # <generator object <genexpr> at 0x...>
print(type(carres_gen)) # <class 'generator'>
# Convertir en liste
print(list(carres_gen)) # [0, 1, 4, 9, 16]nombres = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Avec compréhension de liste
pairs_doubles_liste = [x * 2 for x in nombres if x % 2 == 0]
# Avec expression génératrice
pairs_doubles_gen = (x * 2 for x in nombres if x % 2 == 0)
print(list(pairs_doubles_gen)) # [4, 8, 12, 16, 20]# ❌ Crée inutilement une liste
somme = sum([x ** 2 for x in range(1000000)])
# ✅ Plus efficace : utilise un générateur
somme = sum(x ** 2 for x in range(1000000))
# Note : pas besoin de parenthèses supplémentaires dans sum()nombres = range(20)
# Filtrer les pairs
pairs = (x for x in nombres if x % 2 == 0)
# Mettre au carré
carres = (x ** 2 for x in pairs)
# Filtrer ceux supérieurs à 50
grands = (x for x in carres if x > 50)
# Seules les valeurs nécessaires sont calculées
print(list(grands)) # [64, 100, 144, 196, 256, 324]Python intègre plusieurs fonctions qui fonctionnent bien avec les générateurs :
nombres = range(10)
# map() retourne un itérateur (similaire à un générateur)
doubles = map(lambda x: x * 2, nombres)
print(list(doubles)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# filter() aussi
pairs = filter(lambda x: x % 2 == 0, nombres)
print(list(pairs)) # [0, 2, 4, 6, 8] prenoms = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
# zip() retourne un itérateur
couples = zip(prenoms, ages)
for prenom, age in couples:
print(f"{prenom} a {age} ans")
# Alice a 25 ans
# Bob a 30 ans
# Charlie a 35 ansfruits = ["pomme", "banane", "orange"]
# enumerate() retourne un itérateur
for index, fruit in enumerate(fruits, start=1):
print(f"{index}. {fruit}")
# 1. pomme
# 2. banane
# 3. orangenombres = [1, 2, 3, 4, 5]
# reversed() retourne un itérateur
for n in reversed(nombres):
print(n, end=" ") # 5 4 3 2 1def lire_lignes(nom_fichier):
"""Génère les lignes d'un fichier une par une."""
with open(nom_fichier, 'r', encoding='utf-8') as fichier:
for ligne in fichier:
yield ligne.strip()
# Utilisation efficace en mémoire
# for ligne in lire_lignes('grand_fichier.txt'):
# if 'erreur' in ligne.lower():
# print(ligne)def paginer(elements, taille_page):
"""Génère des pages d'éléments."""
for i in range(0, len(elements), taille_page):
yield elements[i:i + taille_page]
# Exemple
items = list(range(1, 26)) # 25 items
for numero_page, page in enumerate(paginer(items, taille_page=5), start=1):
print(f"Page {numero_page}: {page}")
# Page 1: [1, 2, 3, 4, 5]
# Page 2: [6, 7, 8, 9, 10]
# Page 3: [11, 12, 13, 14, 15]
# Page 4: [16, 17, 18, 19, 20]
# Page 5: [21, 22, 23, 24, 25]def lire_nombres():
"""Simule la lecture de données."""
for i in range(1, 11):
yield i
def filtrer_pairs(nombres):
"""Filtre les nombres pairs."""
for n in nombres:
if n % 2 == 0:
yield n
def multiplier_par_10(nombres):
"""Multiplie chaque nombre par 10."""
for n in nombres:
yield n * 10
# Pipeline de traitement
donnees = lire_nombres()
pairs = filtrer_pairs(donnees)
resultat = multiplier_par_10(pairs)
print(list(resultat)) # [20, 40, 60, 80, 100]def generer_utilisateurs(n):
"""Génère n utilisateurs de test."""
for i in range(1, n + 1):
yield {
"id": i,
"nom": f"Utilisateur_{i}",
"email": f"user{i}@example.com",
"actif": i % 2 == 0
}
# Utilisation
for utilisateur in generer_utilisateurs(5):
print(utilisateur)
# {'id': 1, 'nom': 'Utilisateur_1', 'email': 'user1@example.com', 'actif': False}
# {'id': 2, 'nom': 'Utilisateur_2', 'email': 'user2@example.com', 'actif': True}
# ...import os
def parcourir_fichiers(repertoire, extension=None):
"""Génère tous les fichiers d'un répertoire et ses sous-répertoires."""
for racine, dossiers, fichiers in os.walk(repertoire):
for fichier in fichiers:
if extension is None or fichier.endswith(extension):
yield os.path.join(racine, fichier)
# Utilisation
# for fichier_python in parcourir_fichiers('.', '.py'):
# print(fichier_python)Les générateurs peuvent créer des séquences infinies de manière élégante :
def compteur(debut=0, pas=1):
"""Générateur de compteur infini."""
valeur = debut
while True:
yield valeur
valeur += pas
# Utilisation avec limite
c = compteur(10, 2)
for _ in range(5):
print(next(c), end=" ") # 10 12 14 16 18def cycle(iterable):
"""Répète indéfiniment les éléments d'un itérable."""
while True:
for element in iterable:
yield element
# Utilisation
couleurs = cycle(['rouge', 'vert', 'bleu'])
for _ in range(8):
print(next(couleurs), end=" ")
# rouge vert bleu rouge vert bleu rouge vertdef repeter(valeur, n=None):
"""Répète une valeur n fois (ou indéfiniment si n=None)."""
if n is None:
while True:
yield valeur
else:
for _ in range(n):
yield valeur
# Répétition infinie
# zeros = repeter(0)
# Répétition limitée
cinq_fois = repeter("Python", 5)
print(list(cinq_fois)) # ['Python', 'Python', 'Python', 'Python', 'Python'] Les générateurs ont des méthodes spéciales pour contrôler leur exécution :
def generateur_avec_send():
"""Générateur qui peut recevoir des valeurs."""
total = 0
while True:
valeur = yield total
if valeur is not None:
total += valeur
gen = generateur_avec_send()
# Démarrer le générateur
print(next(gen)) # 0
# Envoyer des valeurs
print(gen.send(10)) # 10
print(gen.send(5)) # 15
print(gen.send(3)) # 18 def mon_generateur():
"""Générateur avec gestion de la fermeture."""
try:
for i in range(10):
yield i
finally:
print("Générateur fermé")
gen = mon_generateur()
print(next(gen)) # 0
print(next(gen)) # 1
# Fermer le générateur
gen.close() # Affiche : Générateur fermé
# Essayer d'obtenir une autre valeur lève StopIteration
# print(next(gen)) # StopIterationdef generateur_resilient():
"""Générateur qui gère les exceptions."""
while True:
try:
valeur = yield
print(f"Reçu : {valeur}")
except ValueError:
print("Erreur ValueError capturée !")
gen = generateur_resilient()
next(gen) # Démarrer
gen.send(10) # Reçu : 10
gen.throw(ValueError, "Une erreur") # Erreur ValueError capturée !
gen.send(20) # Reçu : 20 yield from permet à un générateur de déléguer une partie de ses opérations à un autre générateur :
def generateur1():
yield 1
yield 2
def generateur2():
yield 3
yield 4
# Sans yield from
def combine_manuel():
for valeur in generateur1():
yield valeur
for valeur in generateur2():
yield valeur
# Avec yield from (plus élégant)
def combine_auto():
yield from generateur1()
yield from generateur2()
print(list(combine_auto())) # [1, 2, 3, 4]def aplatir(liste_imbriquee):
"""Aplatit une liste de listes."""
for element in liste_imbriquee:
if isinstance(element, list):
yield from aplatir(element) # Récursion
else:
yield element
# Utilisation
donnees = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]
print(list(aplatir(donnees))) # [1, 2, 3, 4, 5, 6, 7, 8, 9] class Noeud:
def __init__(self, valeur, enfants=None):
self.valeur = valeur
self.enfants = enfants or []
def parcourir_arbre(noeud):
"""Parcourt un arbre en profondeur."""
yield noeud.valeur
for enfant in noeud.enfants:
yield from parcourir_arbre(enfant)
# Création d'un arbre
# 1
# / \
# 2 3
# / \
# 4 5
arbre = Noeud(1, [
Noeud(2, [Noeud(4), Noeud(5)]),
Noeud(3)
])
print(list(parcourir_arbre(arbre))) # [1, 2, 4, 5, 3]✅ Utilisez une liste quand :
- Vous devez accéder aux éléments plusieurs fois
- Vous avez besoin d'accéder à des éléments par index
- Vous devez connaître la longueur (
len()) - Vous devez modifier les éléments
- La collection est petite et tient en mémoire
# Liste appropriée ici
nombres = [1, 2, 3, 4, 5]
print(nombres[2]) # Accès par index
print(len(nombres)) # Longueur
print(nombres * 2) # Duplication ✅ Utilisez un générateur quand :
- Vous parcourez les éléments une seule fois
- Vous traitez de grandes quantités de données
- Vous voulez économiser la mémoire
- Vous travaillez avec des séquences infinies
- Vous créez un pipeline de traitement
# Générateur approprié ici
def lire_grand_fichier():
with open('huge_file.txt', encoding='utf-8') as f:
for ligne in f:
yield ligne.strip()
# Économise énormément de mémoire
# for ligne in lire_grand_fichier():
# traiter(ligne)import sys
# Liste : toutes les valeurs en mémoire
liste = [i for i in range(1000000)]
print(f"Liste : {sys.getsizeof(liste) / 1024 / 1024:.2f} MB")
# Générateur : seulement l'objet générateur
gen = (i for i in range(1000000))
print(f"Générateur : {sys.getsizeof(gen) / 1024:.2f} KB")
# Différence massive !Python fournit le module itertools avec des outils puissants pour travailler avec des itérateurs :
import itertools
# count() - Compteur infini
compteur = itertools.count(10, 2) # Commence à 10, incrémente de 2
for i in range(5):
print(next(compteur), end=" ") # 10 12 14 16 18
print()
# cycle() - Répète indéfiniment
couleurs = itertools.cycle(['R', 'G', 'B'])
for i in range(7):
print(next(couleurs), end=" ") # R G B R G B R
print()
# repeat() - Répète une valeur
for x in itertools.repeat("Python", 3):
print(x, end=" ") # Python Python Python
print()
# chain() - Enchaîne des itérables
liste1 = [1, 2, 3]
liste2 = [4, 5, 6]
for x in itertools.chain(liste1, liste2):
print(x, end=" ") # 1 2 3 4 5 6
print()
# islice() - Découpe un itérable
nombres = range(100)
for x in itertools.islice(nombres, 5, 10): # Éléments 5 à 9
print(x, end=" ") # 5 6 7 8 9
print()
# takewhile() - Prend tant qu'une condition est vraie
nombres = [1, 4, 6, 4, 1]
for x in itertools.takewhile(lambda x: x < 5, nombres):
print(x, end=" ") # 1 4
print()
# dropwhile() - Ignore tant qu'une condition est vraie
nombres = [1, 4, 6, 4, 1]
for x in itertools.dropwhile(lambda x: x < 5, nombres):
print(x, end=" ") # 6 4 1
print()# ✅ Bon : nom explicite
def generer_utilisateurs_actifs(utilisateurs):
for user in utilisateurs:
if user.actif:
yield user
# ❌ Moins clair
def get_users(users):
for u in users:
if u.actif:
yield udef generer_fibonacci(n):
"""
Génère les n premiers nombres de Fibonacci.
Args:
n (int): Nombre de termes à générer
Yields:
int: Prochain nombre de Fibonacci
Example:
>>> list(generer_fibonacci(5))
[0, 1, 1, 2, 3]
"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + bdef lire_fichier_securise(nom_fichier):
"""Lit un fichier ligne par ligne de manière sécurisée."""
try:
fichier = open(nom_fichier, 'r', encoding='utf-8')
try:
for ligne in fichier:
yield ligne.strip()
finally:
fichier.close()
except FileNotFoundError:
print(f"Fichier {nom_fichier} introuvable")
return
# Ou mieux, avec with :
def lire_fichier_securise_v2(nom_fichier):
"""Version améliorée avec context manager."""
with open(nom_fichier, 'r', encoding='utf-8') as fichier:
for ligne in fichier:
yield ligne.strip()# ❌ Inutile de convertir en liste si on itère une fois
nombres = (x ** 2 for x in range(1000))
liste_nombres = list(nombres) # Consomme la mémoire
for n in liste_nombres:
print(n)
# ✅ Mieux : itérer directement
nombres = (x ** 2 for x in range(1000))
for n in nombres:
print(n)gen = (x for x in range(5))
# Premier parcours : fonctionne
print(list(gen)) # [0, 1, 2, 3, 4]
# Deuxième parcours : vide !
print(list(gen)) # []
# Solution : recréer le générateur ou utiliser une liste
gen = (x for x in range(5)) # Recréer# ❌ Erreur
gen = (x for x in range(5))
somme = sum(gen)
liste = list(gen) # Vide !
# ✅ Solution
gen = (x for x in range(5))
liste = list(gen) # Convertir d'abord
somme = sum(liste) gen = (x for x in range(10))
# ❌ Erreur : les générateurs ne supportent pas l'indexation
# print(gen[5]) # TypeError
# ✅ Solution : convertir en liste
liste = list(gen)
print(liste[5]) gen = (x for x in range(10))
# ❌ Erreur : pas de len() pour les générateurs
# print(len(gen)) # TypeError
# ✅ Solution : convertir en liste (si vraiment nécessaire)
liste = list(gen)
print(len(liste)) Dans ce chapitre, nous avons exploré les générateurs en profondeur :
Générateurs :
- Fonctions qui utilisent
yieldau lieu dereturn - Produisent des valeurs à la demande (lazy evaluation)
- Économisent énormément de mémoire
- Permettent de créer des séquences infinies
Expressions génératrices :
- Syntaxe compacte :
(expression for item in iterable) - Alternative légère aux compréhensions de listes
- Idéales pour un usage unique
yield from :
- Délègue à un sous-générateur
- Utile pour la composition et la récursion
- Rend le code plus lisible
✅ Efficacité mémoire : Ne stocke pas toutes les valeurs
✅ Performance : Calcul à la demande
✅ Élégance : Code plus lisible et expressif
✅ Flexibilité : Séquences infinies possibles
✅ Pipeline : Facile à chaîner et composer
- Traitement de fichiers volumineux
- Flux de données continus
- Pipelines de transformation
- Économie de mémoire critique
- Séquences potentiellement infinies
- Les générateurs s'épuisent après utilisation
- Pas d'accès par index ou
len() - Utiliser
list()uniquement si nécessaire - Le module
itertoolsoffre des outils puissants - Toujours documenter les générateurs
Les générateurs sont un outil essentiel pour écrire du code Python performant et élégant, particulièrement pour le traitement de grandes quantités de données !
Dans le prochain chapitre, nous explorerons les closures et la programmation fonctionnelle, pour compléter notre maîtrise des concepts avancés de Python.