Définition
Parcourir une liste avec une boucle for semble aller de soi, mais il faut bien que quelqu'un retienne la position déjà atteinte, sinon chaque tour repartirait du premier élément. Ce quelqu'un, c'est l'itérateur : un objet qui livre les éléments d'une source un par un.
Son vocabulaire tient en deux appels : iter() renvoie l'itérateur lui-même, et next() livre l'élément suivant en avançant le curseur d'un cran. Ce contrat minuscule suffit à parcourir une liste de trois éléments comme un fichier de plusieurs gigaoctets qui ne tiendrait jamais en mémoire.
nombres = [10, 20, 30]
curseur = iter(nombres)
next(curseur) # 10
next(curseur) # 20
next(curseur) # 30L'image du curseur explique pourquoi l'objet ne contient aucune valeur : il avance dans une source qui existe ailleurs. Arrivé au bout, il lève StopIteration, le signal convenu de la fin de course.
Itérable et itérateur ne sont pas la même chose
Les deux mots se ressemblent, ce qui explique une bonne partie des comportements jugés bizarres en apprenant le langage. Un itérable est une source : on peut lui réclamer un curseur neuf autant de fois qu'on le souhaite. L'itérateur est ce curseur, et il ne sert qu'une fois.
Le tableau qui suit résume la différence là où elle compte le plus.
| Question | Itérable | Itérateur |
|---|---|---|
| Exemples courants | Liste, tuple, chaîne, dictionnaire | Résultat de iter(), générateur, fichier ouvert |
Répond à next() | Non | Oui |
| Se parcourt deux fois | Oui, indéfiniment | Non, une seule fois |
| Connaît sa taille | Souvent, via len | Presque jamais |
Une liste fabrique un curseur neuf à chaque parcours, d'où sa lecture indéfinie. Un générateur ou un fichier ouvert est son propre curseur : une fois vidé, il le reste, et rien ne le rembobine.
Ce que fait vraiment une boucle for
La boucle for semble savoir parcourir n'importe quoi comme par magie, mais elle n'invente rien : elle réclame un curseur, appelle next() tant qu'elle obtient une valeur, et sort au signal de fin. Les lignes ci-dessous traduisent ce que fait for element in collection:.
curseur = iter(collection)
while True:
try:
element = next(curseur)
except StopIteration: # le signal de fin, pas une erreur
break
traiter(element)Cette équivalence a une conséquence directe : tout objet qui respecte le protocole se parcourt avec la même écriture, que la boucle traite une liste en mémoire, un fichier sur le disque ou un flux réseau au compte-gouttes.
Prendre la main sur le curseur
Rien n'oblige à laisser la boucle tout faire. Puisque le curseur garde sa position, on peut le récupérer soi-même, consommer quelques éléments à part, puis confier le reste au parcours habituel, qui reprendra là où on l'a laissé. Exemple courant : la ligne d'en-tête d'un fichier de données, à ne pas traiter comme les autres.
with open("ventes.csv") as fichier:
curseur = iter(fichier)
entete = next(curseur) # la première ligne, mise de côté
for ligne in curseur:
traiter(ligne)C'est la mémoire de position qui rend ce découpage possible : le même objet passe d'une main à l'autre sans rien perdre du chemin parcouru. Une liste ordinaire ne sait pas faire cela sans découpage explicite ni compteur à tenir à jour.
Le piège de l'objet déjà consommé
Un code qui fonctionne à la première lecture et renvoie du vide à la seconde, sans la moindre erreur : voilà le piège le plus fréquent autour des itérateurs. Il vient d'une confusion facile, puisque Python renvoie aujourd'hui des itérateurs là où d'anciens tutoriels montrent des listes, avec map, filter, zip, enumerate et tous les générateurs.
paires = zip([1, 2, 3], "abc")
print(list(paires)) # [(1, 'a'), (2, 'b'), (3, 'c')]
print(list(paires)) # [] : le curseur est déjà videLe défaut ne se voit jamais où il est commis : la première lecture réussit, souvent loin de l'objet créé, et la seconde remonte une collection vide sans avertissement. Un réflexe règle la question : matérialiser avec list() par avance, ou reconstruire l'objet à chaque parcours si le volume interdit de tout garder en mémoire.
Un itérateur reste vrai dans une condition même vide, faute de longueur ou de valeur de vérité propre : if resultats: passe alors qu'il ne reste rien à lire, un test habituellement fiable qui devient ici trompeur.
Écrire le sien
Comprendre le protocole permet de l'implémenter soi-même. Un objet devient itérateur dès qu'il expose __iter__, qui renvoie l'objet lui-même, et __next__, qui livre la valeur suivante ou lève le signal de fin. Chacune est une méthode magique, appelée par Python, jamais par le code qui s'en sert.
class Compteur:
def __init__(self, fin):
self.valeur = 0
self.fin = fin
def __iter__(self):
return self # la classe est son propre curseur
def __next__(self):
if self.valeur >= self.fin:
raise StopIteration
self.valeur += 1
return self.valeurCette classe se parcourt comme n'importe quelle collection. En pratique, un yield dans une fonction produit le même résultat en trois lignes : c'est pourquoi le générateur a presque remplacé la classe écrite à la main.
Questions fréquentes
Pourquoi ma liste de résultats est-elle vide au deuxième passage ?
Parce que l'objet parcouru était un itérateur, pas une collection. Le premier passage l'a vidé, et le second ne trouve plus rien à lire, sans qu'aucune erreur ne le signale. Un list() posé sur le résultat dès sa production règle le problème définitivement.
Comment connaître le nombre d'éléments restants ?
Aucun moyen ne permet de le savoir sans consommer l'objet, et len() échoue dessus. La seule réponse consiste à le matérialiser en liste, donc à charger tout son contenu en mémoire, ce que l'itérateur cherchait justement à éviter.
Faut-il préférer un itérateur à une liste ?
Dès que les données sont volumineuses ou lues une seule fois, oui : la mémoire occupée reste constante quel que soit le volume traité. Pour quelques dizaines d'éléments relus plusieurs fois, la liste reste plus simple et plus rapide.