Un itérateur en Python : l'objet qui avance dans une collection

Un itérateur avance dans une source et retient sa position. Pourquoi il ne se parcourt qu'une seule fois, et comment écrire le sien.
5 min de lecture
Believemy logo

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.

PYTHON
nombres = [10, 20, 30]
curseur = iter(nombres)

next(curseur)  # 10
next(curseur)  # 20
next(curseur)  # 30

L'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.

QuestionItérableItérateur
Exemples courantsListe, tuple, chaîne, dictionnaireRésultat de iter(), générateur, fichier ouvert
Répond à next()NonOui
Se parcourt deux foisOui, indéfinimentNon, une seule fois
Connaît sa tailleSouvent, via lenPresque 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:.

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

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

PYTHON
paires = zip([1, 2, 3], "abc")

print(list(paires))  # [(1, 'a'), (2, 'b'), (3, 'c')]
print(list(paires))  # [] : le curseur est déjà vide

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

Attention

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.

PYTHON
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.valeur

Cette 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

Question

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.

Question

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.

Question

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.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.