Générateurs
& Itérateurs
Parcourir des données sans tout charger en mémoire — yield, protocole itérable et itertools.
Protocole itérable
En Python, tout objet que l'on peut parcourir avec for implémente le protocole itérable via deux méthodes spéciales : __iter__ et __next__.
nombres = [1, 2, 3]
# La boucle for fait exactement ceci :
iterateur = iter(nombres) # appelle __iter__
while True:
try:
valeur = next(iterateur) # appelle __next__
print(valeur)
except StopIteration: # fin de l'itération
break
# Distinctions importantes
# Itérable : a __iter__ — peut créer un itérateur
# (list, tuple, str, dict, set, fichier…)
# Itérateur : a __iter__ ET __next__
# retourne iter(obj) == obj
it = iter([1, 2, 3])
print(iter(it) is it) # True — l'itérateur est itérable
| Objet | Itérable ? | Itérateur ? |
|---|---|---|
list, tuple | ✓ | ✗ — crée un itérateur à la demande |
str, bytes | ✓ | ✗ |
dict, set | ✓ | ✗ |
iter(liste) | ✓ | ✓ — exhaustible, à usage unique |
| Fichier ouvert | ✓ | ✓ — ligne par ligne |
| Générateur | ✓ | ✓ — paresseux |
range(n) | ✓ | ✗ — réutilisable, optimisé |
Un itérateur est à usage unique — une fois épuisé, il lève StopIteration à chaque next(). Une liste peut créer autant d'itérateurs qu'on veut.
Itérateur personnalisé
class Compte:
"""Itérateur qui compte de debut à fin."""
def __init__(self, debut: int, fin: int) -> None:
self.courant = debut
self.fin = fin
def __iter__(self):
return self # l'objet est son propre itérateur
def __next__(self) -> int:
if self.courant > self.fin:
raise StopIteration
valeur = self.courant
self.courant += 1
return valeur
for n in Compte(1, 5):
print(n) # 1 2 3 4 5
print(list(Compte(1, 3))) # [1, 2, 3]
print(sum(Compte(1, 100))) # 5050
class Fibonacci:
"""Itérable infini — non exhaustible."""
def __iter__(self):
return FibonacciIterateur() # crée un nouvel itérateur
class FibonacciIterateur:
def __init__(self) -> None:
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self) -> int:
valeur = self.a
self.a, self.b = self.b, self.a + self.b
return valeur
import itertools
fib = Fibonacci()
# 10 premiers termes
print(list(itertools.islice(fib, 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
En pratique, on n'implémente presque jamais __iter__/__next__ manuellement — les générateurs (yield) font la même chose avec 10× moins de code.
yield — principe et mécanique
yield suspend l'exécution de la fonction, retourne une valeur, et reprend exactement là où elle s'était arrêtée au prochain next(). La fonction devient un générateur.
def generateur_simple():
print("Début")
yield 1 # suspend ici
print("Après 1")
yield 2 # suspend ici
print("Fin")
# return implicite → StopIteration
gen = generateur_simple()
print(type(gen)) # <class 'generator'>
print(next(gen)) # Début → 1
print(next(gen)) # Après 1 → 2
print(next(gen)) # Fin → StopIteration !
# La fonction n'est pas exécutée avant next()
gen2 = generateur_simple() # rien n'est imprimé
for v in gen2: # exécution pilotée par for
print(v)
Fonctions génératrices — cas d'usage
from pathlib import Path
def lire_lignes_filtrees(chemin: str, prefixe: str):
"""Lit un fichier et yield seulement les lignes qui commencent par prefixe."""
with open(chemin, encoding="utf-8") as f:
for ligne in f:
ligne = ligne.rstrip()
if ligne.startswith(prefixe):
yield ligne
# Le fichier reste ouvert pendant toute l'itération
# et est fermé proprement à la fin du with
# Utilisation — ne charge jamais tout en mémoire
for erreur in lire_lignes_filtrees("app.log", "ERROR"):
print(erreur)
def lire_csv(chemin):
import csv
with open(chemin, encoding="utf-8") as f:
yield from csv.DictReader(f)
def filtrer(lignes, champ, valeur):
for l in lignes:
if l[champ] == valeur:
yield l
def transformer(lignes):
for l in lignes:
yield {**l, "salaire": float(l["salaire"]) * 1.05}
# Pipeline — chaque étape est paresseuse
resultats = transformer(
filtrer(lire_csv("employes.csv"), "dept", "IT")
)
for emp in resultats:
print(emp)
import sys
# Liste : tous les éléments en mémoire
liste = [x ** 2 for x in range(1_000_000)]
print(sys.getsizeof(liste)) # ≈ 8 000 056 octets
# Générateur : un seul élément en mémoire à la fois
gen = (x ** 2 for x in range(1_000_000))
print(sys.getsizeof(gen)) # ≈ 104 octets !
# sum() fonctionne avec les deux
print(sum(liste)) # 333332833333500000
print(sum(x ** 2 for x in range(1_000_000))) # idem
Règle pratique : utiliser une liste quand on a besoin d'accéder plusieurs fois aux données, d'indexer, ou de connaître la longueur. Utiliser un générateur pour traiter une grande quantité de données en un seul passage.
def id_auto(debut: int = 1):
"""Génère des IDs uniques à l'infini."""
n = debut
while True:
yield n
n += 1
gen = id_auto()
print(next(gen)) # 1
print(next(gen)) # 2
# Limiter avec itertools.islice
import itertools
print(list(itertools.islice(id_auto(100), 5))) # [100..104]
send() & throw() — générateurs bidirectionnels
def accumulateur():
"""Reçoit des valeurs et retourne le cumul."""
total = 0
while True:
valeur = yield total # yield retourne ET reçoit
if valeur is None:
break
total += valeur
gen = accumulateur()
next(gen) # démarrer (obligatoire)
print(gen.send(10)) # 10
print(gen.send(5)) # 15
print(gen.send(20)) # 35
def moniteur():
try:
while True:
valeur = yield
print(f"Reçu : {valeur}")
except GeneratorExit:
print("Moniteur fermé proprement")
except ValueError as e:
print(f"Erreur injectée : {e}")
gen = moniteur()
next(gen)
gen.send("données A") # Reçu : données A
gen.throw(ValueError, "valeur incorrecte")
# Erreur injectée : valeur incorrecte
gen.close() # Moniteur fermé proprement
send(), throw() et close() sont rarement utilisés directement — ils forment la base sur laquelle est construit asyncio. Les coroutines async/await utilisent des générateurs sous le capot.
yield from — délégation
yield from délègue l'itération à un sous-itérable — il yield tous ses éléments un à un, et transmet aussi les send() et throw() au sous-générateur.
def aplatir(structure):
"""Aplatit récursivement des listes imbriquées."""
for element in structure:
if isinstance(element, list):
yield from aplatir(element) # récursion
else:
yield element
data = [1, [2, 3], [4, [5, 6]], 7]
print(list(aplatir(data))) # [1, 2, 3, 4, 5, 6, 7]
# Sans yield from — plus verbeux
def aplatir_v2(structure):
for el in structure:
if isinstance(el, list):
for sous_el in aplatir_v2(el): # double boucle
yield sous_el
else:
yield el
def concatener(*iterables):
"""Équivalent à itertools.chain."""
for it in iterables:
yield from it
r = list(concatener([1, 2], (3, 4), "ab"))
print(r) # [1, 2, 3, 4, 'a', 'b']
# yield from avec valeur de retour
def sous_gen():
yield 1
yield 2
return "terminé" # valeur retournée à yield from
def gen_principale():
resultat = yield from sous_gen()
print(f"Sous-gen a retourné : {resultat}")
yield 3
print(list(gen_principale()))
# Sous-gen a retourné : terminé
# [1, 2, 3]
Expressions génératrices
# List comprehension → liste en mémoire [ ]
carres_liste = [x**2 for x in range(10)]
# Expression génératrice → paresseuse ( )
carres_gen = (x**2 for x in range(10))
# Quand passer à une fonction qui consume entièrement :
# les parenthèses supplémentaires sont facultatives
total = sum(x**2 for x in range(1000)) # ✓ idiomatique
maxi = max(len(mot) for mot in texte.split())
# Avec filtre
pairs = (x for x in range(100) if x % 2 == 0)
# Convertir en liste quand nécessaire
liste = list(pairs) # consomme le générateur
liste = list(pairs) # [] — générateur épuisé !
# Lire des lignes non vides d'un fichier
with open("data.txt", encoding="utf-8") as f:
lignes = (l.strip() for l in f if l.strip())
# Trouver le premier élément correspondant
nombres = range(1, 1000)
premier_pair_cube = next(
x for x in nombres
if x % 2 == 0 and x ** (1/3) % 1 == 0
)
print(premier_pair_cube) # 8
# any() et all() avec générateur (court-circuit)
mots = ["hello", "world", "Python"]
print(any(m[0].isupper() for m in mots)) # True
print(all(len(m) > 3 for m in mots)) # True
Compréhensions — récapitulatif
donnees = [1, 2, 3, 4, 5, 6]
# List comprehension → list
carres = [x**2 for x in donnees if x % 2 == 0] # [4, 16, 36]
# Set comprehension → set (valeurs uniques)
uniques = {x % 3 for x in donnees} # {0, 1, 2}
# Dict comprehension → dict
carre_dict = {x: x**2 for x in donnees} # {1:1, 2:4, …}
# Inverser un dict
original = {"a": 1, "b": 2, "c": 3}
inverse = {v: k for k, v in original.items()} # {1:'a', 2:'b', 3:'c'}
# Generator expression → générateur (paresseux)
gen = (x**2 for x in donnees)
# Compréhension imbriquée — matrice
matrice = [[1,2,3],[4,5,6],[7,8,9]]
aplatie = [val for ligne in matrice for val in ligne] # [1,2,3,4,5,6,7,8,9]
transposee = [[ligne[i] for ligne in matrice] for i in range(3)]
itertools — itérateurs fondamentaux
import itertools as it
# count : compte à partir de start avec step
for i in it.count(10, 2): # 10, 12, 14, 16, …
if i > 20: break
# cycle : répète l'itérable à l'infini
couleurs = it.cycle(["rouge", "vert", "bleu"])
for _, c in zip(range(7), couleurs):
print(c) # rouge vert bleu rouge vert bleu rouge
# repeat : répète une valeur N fois (ou infiniment)
list(it.repeat(42, 3)) # [42, 42, 42]
# islice : slice paresseuse
list(it.islice(range(100), 5)) # [0,1,2,3,4]
list(it.islice(range(100), 10, 15)) # [10,11,12,13,14]
# takewhile / dropwhile
list(it.takewhile(lambda x: x<5, [1,3,5,2,7])) # [1,3]
list(it.dropwhile(lambda x: x<5, [1,3,5,2,7])) # [5,2,7]
# chain : concaténer des itérables
list(it.chain([1,2], [3,4], [5])) # [1,2,3,4,5]
list(it.chain.from_iterable([[1,2],[3,4]])) # [1,2,3,4]
# groupby : grouper des éléments consécutifs identiques
# ATTENTION : trier avant groupby !
from itertools import groupby
donnees = [
{"nom": "Alice", "dept": "IT"},
{"nom": "Bob", "dept": "IT"},
{"nom": "Carol", "dept": "RH"},
]
donnees.sort(key=lambda x: x["dept"]) # trier d'abord
for dept, groupe in groupby(donnees, key=lambda x: x["dept"]):
membres = [p["nom"] for p in groupe]
print(f"{dept}: {membres}")
# IT: ['Alice', 'Bob']
# RH: ['Carol']
# zip_longest : zip avec valeur par défaut
from itertools import zip_longest
list(zip_longest([1,2,3], ["a","b"], fillvalue=0))
# [(1,'a'), (2,'b'), (3,0)]
itertools — combinatoire
from itertools import product, permutations, combinations, combinations_with_replacement
couleurs = ["R", "V", "B"]
chiffres = [1, 2]
# product : produit cartésien A × B
list(product(couleurs, chiffres))
# [('R',1), ('R',2), ('V',1), ('V',2), ('B',1), ('B',2)]
# permutations : toutes les permutations de r éléments
list(permutations("ABC", 2))
# [('A','B'), ('A','C'), ('B','A'), ('B','C'), ('C','A'), ('C','B')]
# combinations : combinaisons sans répétition (ordre ignoré)
list(combinations("ABCD", 2))
# [('A','B'), ('A','C'), ('A','D'), ('B','C'), ('B','D'), ('C','D')]
# combinations_with_replacement : avec répétition
list(combinations_with_replacement("ABC", 2))
# [('A','A'), ('A','B'), ('A','C'), ('B','B'), ('B','C'), ('C','C')]
| Fonction | Formule | Exemple n=4, r=2 |
|---|---|---|
product(A, repeat=r) | nr | 16 éléments |
permutations(A, r) | n! / (n-r)! | 12 éléments |
combinations(A, r) | n! / r!(n-r)! | 6 éléments |
combinations_with_replacement(A, r) | (n+r-1)! / r!(n-1)! | 10 éléments |
Cheat sheet
Protocole itérable
| __iter__(self) | Retourner l'itérateur |
| __next__(self) | Élément suivant ou StopIteration |
| iter(obj) | Obtenir l'itérateur |
| next(it) | Élément suivant |
| next(it, défaut) | Sans exception si vide |
yield
| yield valeur | Suspendre + retourner |
| yield from it | Déléguer à un sous-itérable |
| gen.send(val) | Envoyer une valeur |
| gen.throw(Ex) | Injecter une exception |
| gen.close() | Fermer proprement |
Compréhensions
| [expr for x in it] | Liste |
| {expr for x in it} | Set |
| {k:v for x in it} | Dict |
| (expr for x in it) | Générateur (paresseux) |
| … if condition | Filtre optionnel |
itertools essentiels
| islice(it, n) | Trancher un générateur |
| chain(*its) | Concaténer |
| groupby(it, key) | Grouper (trier avant !) |
| product(A, B) | Produit cartésien |
| combinations(A, r) | Combinaisons |
| cycle(it) | Répétition infinie |