Cours de Python / Parcourir les données avec itérateurs et générateurs

Un itérateur fournit les éléments d’un parcours un à un ; un générateur permet d’écrire ce comportement avec yield. Les valeurs peuvent être calculées à la demande, mais un parcours épuisé ne recommence pas automatiquement depuis le début.

Distinguer l’objet parcourable du parcours en cours

Une liste est un objet itérable : on peut demander un itérateur avec iter(liste). Cet itérateur conserve une position dans le parcours. next demande l’élément suivant ; lorsqu’il n’y en a plus, il lève StopIteration. La boucle for utilise ce protocole et traite ce signal pour terminer normalement.

Un générateur est lui-même un itérateur. Appeler la fonction qui contient yield crée un objet générateur ; son corps commence à s’exécuter lorsqu’une valeur est demandée. À chaque yield, il fournit une valeur puis suspend son exécution en conservant l’état local nécessaire pour continuer. Dans l’exemple, la variable total garde ainsi la somme déjà calculée.

Contrairement à un return qui termine l’appel, yield peut être atteint plusieurs fois lors des demandes successives. Pour une présentation du mécanisme, consultez le tutoriel officiel sur les générateurs Python.

Consommer des totaux cumulés étape par étape

Enregistrez ce programme complet en UTF-8 dans generators.py, dans un dossier d’exercice. Aucune dépendance externe n’est requise. Dans un terminal ouvert dans ce dossier avec l’environnement virtuel du projet sélectionné, lancez python generators.py. Utilisez python3 generators.py si python3 est votre commande hors environnement virtuel sous macOS ou Ubuntu.

def running_totals(amounts):
    total = 0
    for amount in amounts:
        total += amount
        yield total

values = running_totals([3500, 8000, 2500])
print(next(values))
print(list(values))
print(list(values))
print(list(running_totals([3500, 8000, 2500])))

Résultat attendu :

3500
[11500, 14000]
[]
[3500, 11500, 14000]

Le premier next reçoit 3500 et laisse le générateur suspendu après le premier yield. Le premier list consomme les valeurs restantes : 11500 puis 14000. Le second list reçoit un itérateur déjà épuisé et produit donc une liste vide. La dernière ligne appelle de nouveau running_totals et consomme un nouvel objet ; elle obtient les trois résultats.

Pour observer un changement unique, ajoutez temporairement un deuxième print(next(values)) juste après le premier. Vous obtenez 3500, puis 11500 ; la liste qui suit ne contient plus que [14000]. La nouvelle création finale donne toujours les trois totaux. Retirez l’appel supplémentaire pour rétablir le résultat initial.

Si vous devez relire un petit résultat, vous pouvez aussi le conserver une fois avec results = list(running_totals([3500, 8000, 2500])). Relire results parcourt alors une liste, et non le générateur consommé. Cela a un coût de stockage explicite.

Comparer une liste, son itérateur et une expression génératrice

Enregistrez ce deuxième programme dans un fichier distinct, iterator.py, pour conserver le premier. Depuis le même dossier, lancez python iterator.py ou la commande python3 correspondante.

values = [1, 2, 3]
iterator = iter(values)
print(next(iterator))
print(list(iterator))
print(list(iterator))
print(list(values))
expression = (n * 2 for n in values)
print(list(expression))

Résultat attendu :

1
[2, 3]
[]
[1, 2, 3]
[2, 4, 6]

La liste values conserve ses trois éléments. Seul iterator a avancé : après le premier next, il reste 2 et 3. Une fois ces éléments consommés, le parcours est vide, tandis que list(values) produit encore les trois valeurs. Un nouvel appel iter(values) créerait un autre parcours indépendant.

L’expression (n * 2 for n in values) calcule les doubles à la demande. Ce sont les parenthèses, au lieu des crochets d’une compréhension de liste, qui construisent ici un générateur. Ajoutez temporairement print(list(expression)) à la fin : la ligne supplémentaire affiche [] car l’expression a déjà été consommée. Retirez-la après observation.

L’évaluation différée ne signifie pas que rien ne se passe au moment de créer une expression génératrice : son itérable le plus à gauche est évalué immédiatement. Le calcul des valeurs produites reste différé. La référence des expressions génératrices décrit précisément cette distinction.

Produire les totaux successifs sans tous les stocker peut réduire la mémoire occupée par les résultats intermédiaires. L’entrée de cet exemple est néanmoins déjà une liste, qui reste en mémoire ; convertir le générateur entier avec list stocke aussi tous ses résultats. La paresse change le moment du calcul et le stockage, sans garantir une exécution plus rapide pour toute tâche.

Vérifier les entrées vides, les erreurs différées et les ressources

  1. Dans generators.py, remplacez seulement la liste du premier appel par []. Le premier next lève StopIteration et arrête le script avant les affichages suivants. Rétablissez ensuite les trois montants.
  2. Remplacez cette même liste par [3500, "8000", 2500]. La création du générateur réussit ; le premier next affiche 3500. Le list suivant déclenche TypeError en essayant d’additionner la chaîne. Rétablissez 8000 sans guillemets.
  3. Si l’absence d’un premier élément est un cas normal, essayez temporairement next(values, None) : la valeur par défaut évite StopIteration à cet endroit. Choisissez un marqueur qui ne puisse pas être confondu avec un résultat valide, puis restaurez le programme initial.

Dans la fonction de cet exemple, l’erreur de calcul se produit à la consommation. Placez donc la gestion des erreurs autour de la boucle ou du consommateur concerné, et pas seulement autour de la création du générateur. L’appel ne garantit pas à lui seul que toutes les valeurs futures seront calculables.

Pour les fichiers, prévoyez également la durée de vie de la ressource. Une boucle interrompue avec break ne ferme pas à elle seule un générateur qui conserve un fichier ouvert. Lorsque c’est possible, ouvrez le fichier dans un bloc with contrôlé par l’appelant et consommez ses lignes à l’intérieur de ce bloc. La gestion des fichiers texte avec with explique ce contrôle explicite ; ne renvoyez pas un parcours qui dépend d’un fichier déjà fermé.