Les itérateurs Python
Apprenez le fonctionnement des itérateurs Python, comment créer des classes d'itérateurs personnalisées et quand les utiliser à la place des listes.
Un itérateur est l'une des abstractions les plus fondamentales de Python. Chaque fois que vous écrivez une boucle for, appelez zip(), ou utilisez une compréhension de liste, Python s'appuie silencieusement sur le protocole itérateur en coulisses. Ce chapitre explique ce que sont les itérateurs, comment construire les vôtres, comment utiliser le riche ensemble d'itérateurs intégrés, et quand les itérateurs sont le bon outil pour le travail.
Qu'est-ce qu'un itérateur ?
Python distingue deux concepts liés :
- Un itérable est tout objet sur lequel vous pouvez itérer — une
list, untuple, unstr, undict, unset, ou tout objet dont la classe définit__iter__. Il peut produire un itérateur, mais ne suit pas lui-même sa position. - Un itérateur est un objet qui suit l'état du parcours. Il implémente deux méthodes qui forment ensemble le protocole itérateur :
__iter__()— renvoie l'objet itérateur lui-même. Cela permet aux itérateurs de fonctionner dans les bouclesforet autres contextes d'itération.__next__()— renvoie la valeur suivante à chaque appel. Lorsqu'il ne reste plus de valeurs, elle lèveStopIteration.
La différence clé : vous pouvez parcourir une liste autant de fois que vous le souhaitez, car chaque boucle for demande un nouvel itérateur. Un itérateur est unidirectionnel et à usage unique — une fois épuisé, appeler next() dessus lève toujours StopIteration.
graph LR
A[Iterator Object] --> B[__iter__]
B --> C[Returns self]
A --> D[__next__]
D --> E[Next Value]
D --> F{No values left?}
F -->|Yes| G[Raises StopIteration]
F -->|No| EComment une boucle for utilise les itérateurs
La boucle for n'est qu'un sucre syntaxique pour le protocole itérateur. En interne, Python traduit :
for item in some_iterable:
print(item)en approximativement ceci :
_it = iter(some_iterable) # call __iter__()
while True:
try:
item = next(_it) # call __next__()
except StopIteration:
break
print(item)Comprendre cette traduction permet de voir clairement pourquoi tout objet qui implémente __iter__ et __next__ fonctionne de façon transparente dans une boucle for, avec zip(), enumerate(), et tout autre contexte qui attend un itérable.
Construire un itérateur personnalisé
Pour créer un itérateur personnalisé, définissez une classe qui implémente à la fois __iter__ et __next__. Voici un itérateur Countdown qui compte à rebours depuis un nombre donné jusqu'à 1 :
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # the iterator is its own iterable
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(5):
print(n)
# Output:
# 5
# 4
# 3
# 2
# 1Notez que __iter__ renvoie self. C'est ce qui permet de placer le même objet directement dans une boucle for — la boucle appelle iter() dessus, ce qui appelle __iter__(), qui renvoie l'itérateur lui-même.
Ajouter un paramètre de pas
Vous pouvez ajouter toute logique souhaitée à l'intérieur de __next__. Voici un itérateur StepRange qui imite range() mais accepte une valeur de pas :
class StepRange:
def __init__(self, start, stop, step=1):
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
print(list(StepRange(0, 10, 3)))
# Output: [0, 3, 6, 9]Appeler list() sur n'importe quel itérateur l'épuise et collecte toutes les valeurs dans une liste — un schéma utile quand vous avez besoin de tous les résultats en même temps.
Les fonctions intégrées iter() et next()
Les fonctions intégrées iter() et next() sont le moyen standard de travailler directement avec le protocole itérateur.
iter(obj)— appelleobj.__iter__()et renvoie l'itérateur résultant.next(it)— appelleit.__next__()et renvoie la valeur suivante.next(it, default)— renvoiedefaultau lieu de leverStopIterationquand l'itérateur est épuisé. C'est la façon la plus sûre d'examiner l'élément suivant sans bloc try/except.
words = ["hello", "world"]
it = iter(words)
print(next(it)) # hello
print(next(it)) # world
print(next(it, "done")) # done (exhausted; returns default)La forme à deux arguments de next() est particulièrement utile dans les scénarios de flux ou d'analyse syntaxique où vous souhaitez gérer gracieusement la fin de l'entrée.
Les itérateurs sont à usage unique
C'est le piège le plus courant avec les itérateurs : une fois épuisé, un itérateur ne peut pas être rembobiné.
it = iter([1, 2, 3])
for x in it:
print(x) # prints 1, 2, 3
for x in it:
print(x) # prints nothing — iterator is exhaustedSi vous devez itérer plusieurs fois, conservez l'itérable d'origine (par exemple, la liste) et appelez iter() à nouveau, ou utilisez une compréhension de liste pour matérialiser d'abord toutes les valeurs.
Fonctions intégrées qui renvoient des itérateurs
La bibliothèque standard de Python est construite sur les itérateurs. Ces fonctions renvoient toutes des itérateurs plutôt que des listes, ce qui les rend efficaces en mémoire même pour de très grandes séquences :
range()
range(start, stop, step) renvoie un itérateur d'entiers. Il ne stocke pas les entiers en mémoire — il calcule chacun à la demande.
for i in range(1, 6):
print(i)
# Output: 1 2 3 4 5zip()
zip() prend plusieurs itérables et renvoie un itérateur de tuples, en associant les éléments position par position. L'itération s'arrête à l'entrée la plus courte.
names = ["Alice", "Bob", "Carol"]
scores = [95, 88, 72]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Output:
# Alice: 95
# Bob: 88
# Carol: 72enumerate()
enumerate() enveloppe tout itérable et renvoie des paires (index, valeur). Utilisez-le pour éviter de maintenir une variable compteur manuelle.
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherrymap() et filter()
Les deux fonctions renvoient des itérateurs (en Python 3). map(fn, iterable) applique une fonction à chaque élément ; filter(fn, iterable) ne conserve que les éléments pour lesquels la fonction renvoie True.
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
print(doubled) # [2, 4, 6, 8, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]Vérifier si un objet est un itérateur
Utilisez isinstance() avec les classes de base abstraites du module collections.abc pour tester l'itérabilité et le statut d'itérateur :
from collections.abc import Iterable, Iterator
my_list = [1, 2, 3]
my_iter = iter(my_list)
print(isinstance(my_list, Iterable)) # True — list is iterable
print(isinstance(my_list, Iterator)) # False — list is NOT an iterator
print(isinstance(my_iter, Iterator)) # True — list_iterator is an iterator
print(isinstance(my_iter, Iterable)) # True — all iterators are also iterablesTout itérateur est aussi un itérable (car __iter__ renvoie self), mais tout itérable n'est pas nécessairement un itérateur.
Quand utiliser des itérateurs plutôt que des listes
| Situation | Utiliser |
|---|---|
Besoin d'accès aléatoire (items[5]) | list |
| Besoin d'itérer une seule fois, la mémoire est importante | itérateur / générateur |
| Séquences infinies ou très grandes | itérateur / générateur |
| Besoin d'itérer plusieurs fois | list (conserver l'original) |
| Pipeline de transformations | itérateurs chaînés (map, filter, itertools) |
Pour les grands ensembles de données — lecture de millions de lignes depuis un fichier, traitement de données en flux — un itérateur évite de tout charger en mémoire en même temps. Pour les petites collections finies où vous accédez aux éléments de façon répétée, une liste est plus simple.
Itérateurs vs. générateurs
Un générateur est un raccourci pratique pour écrire un itérateur. Au lieu d'une classe avec __iter__ et __next__, vous écrivez une fonction qui utilise yield. Python la convertit automatiquement en itérateur.
# Iterator class
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
# Equivalent generator function
def countdown(start):
while start > 0:
yield start
start -= 1
print(list(countdown(5))) # [5, 4, 3, 2, 1]Utilisez un itérateur basé sur une classe lorsque vous avez besoin de méthodes supplémentaires ou d'un état mutable au-delà de ce qu'un simple générateur offre. Utilisez un générateur pour la plupart des autres cas — il est plus concis et tout aussi puissant.
Consultez le chapitre Python Generators pour un traitement complet de yield, des expressions de générateur et de send().