Kodokon kodokon.com

Itérateurs et générateurs : yield et pipelines

Maîtrisez le protocole d'itération et les générateurs pour traiter des flux de données paresseusement, à mémoire constante.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

Le for de Python n'est qu'un protocole : iter(obj) demande un itérateur, puis next() est appelé jusqu'à l'exception StopIteration. Distinguez bien l'itérable (la liste, capable de fournir un itérateur neuf à chaque parcours) de l'itérateur (le curseur, qui ne se parcourt qu'une seule fois). Comprendre ce protocole, c'est comprendre pourquoi certains objets « se consomment ».

PYTHON
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
# next(iterator) lèverait StopIteration
Ce que fait un for, à la main

Une fonction qui contient yield ne s'exécute pas à l'appel : elle retourne un générateur, un itérateur dont le corps s'exécute à la demande. Chaque next() avance jusqu'au yield suivant, où l'exécution est suspendue avec tout son état local. Conséquence directe : la mémoire reste constante quelle que soit la taille du flux - vous produisez les valeurs une par une au lieu de matérialiser une liste complète.

PYTHON
def squares(limit):
    n = 0
    while n < limit:
        yield n * n
        n += 1

gen = squares(4)
print(next(gen))  # 0
print(list(gen))  # [1, 4, 9]
yield suspend la fonction entre deux valeurs

L'intérêt majeur en production : les pipelines. Chaque étape consomme le flux de la précédente et produit le sien, paresseusement ; rien ne s'exécute avant la consommation finale (list(), sum(), un for). Compromis à connaître : une liste se parcourt plusieurs fois et se découpe en tranches, un générateur non ; en revanche, le générateur traite un fichier de 10 Go sans saturer la RAM. Les expressions génératrices - (f(x) for x in xs if cond) - couvrent les étapes simples sans définir de fonction.

PYTHON
lines = ["  12 ", "x", " 7", "", "30"]

def clean(rows):
    for row in rows:
        row = row.strip()
        if row.isdigit():
            yield int(row)

def keep_from(values, floor):
    return (v for v in values if v >= floor)

pipeline = keep_from(clean(lines), 10)
print(list(pipeline))  # [12, 30]
Un pipeline paresseux : rien ne tourne avant list()

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Qu'obtenez-vous en appelant une fonction dont le corps contient yield ?
    • Le résultat du premier yield
    • Un générateur, sans exécution du corps
    • Une liste de toutes les valeurs produites
  2. Quel avantage décisif un pipeline de générateurs a-t-il sur des listes intermédiaires ?
    • Les résultats sont automatiquement triés
    • La mémoire reste constante : les valeurs sont produites à la demande
    • Le traitement est parallélisé sur plusieurs cœurs
  3. Après un premier list(gen), que renvoie un second list(gen) sur le même générateur ?
    • La même liste qu'au premier appel
    • Une liste vide
    • Une exception est propagée