Programmation Python

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__.

Ce que fait for … in … en coulisses
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
ObjetIté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é

Implémenter __iter__ & __next__
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
Itérable réutilisable (séparation iter/itérateur)
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.

appelant
next(gen) → démarre / reprend le générateur
générateur
s'exécute jusqu'au prochain yield…
générateur
yield valeur → suspend, retourne valeur
appelant
reçoit valeur, continue son travail
appelant
next(gen) → reprend après le yield
générateur
continue jusqu'au prochain yield ou return
générateur
return → lève StopIteration
yield — étape par étape
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

Lire un grand fichier ligne par ligne
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)
Pipeline de traitement paresseux
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)
Générateur vs liste — mémoire
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.

Générateur infini
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

send() — envoyer une valeur au générateur
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
throw() & close() — contrôle du générateur
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.

yield from — aplatir des structures
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
yield from — chaîner des itérables
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

Syntaxe et différence avec list comprehension
# 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é !
Exemples pratiques
# 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

Les 4 types de compréhensions
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

Itérateurs infinis
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]
Sélection et découpage
# 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]
Groupement et agrégation
# 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

Permutations, combinaisons, produit cartésien
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')]
FonctionFormuleExemple n=4, r=2
product(A, repeat=r)nr16 é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 valeurSuspendre + retourner
yield from itDé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 conditionFiltre 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