35. Comment fonctionne l’itération : itérables et itérateurs
Tout au long de ce livre, vous avez utilisé des boucles for pour itérer sur des listes, des chaînes de caractères, des dictionnaires et d’autres collections. Vous avez écrit du code comme for item in my_list: d’innombrables fois. Mais que se passe-t-il réellement en coulisses lorsque Python exécute une boucle for ? Comment Python sait-il comment parcourir différents types de collections ?
Dans ce chapitre, nous allons explorer le protocole d’itération (iteration protocol) de Python — le mécanisme qui fait fonctionner les boucles for. Vous découvrirez les itérables (iterables) (objets sur lesquels vous pouvez boucler) et les itérateurs (iterators) (objets qui effectuent réellement l’avancement à travers les valeurs). Comprendre cette distinction approfondira votre connaissance du fonctionnement de Python et vous préparera à travailler avec les générateurs au chapitre 36.
35.1) Ce que signifie, pour un objet, être itérable
35.1.1) Le concept d’itérabilité
Un itérable (iterable) est n’importe quel objet Python sur lequel on peut boucler avec une boucle for. Lorsque nous disons « boucler », nous voulons dire que Python peut récupérer les éléments de l’objet un par un, dans l’ordre.
Vous avez déjà travaillé avec de nombreux itérables :
# Les listes sont itérables
numbers = [1, 2, 3, 4, 5]
for num in numbers:
print(num) # Output: 1, 2, 3, 4, 5 (on separate lines)
# Les chaînes de caractères sont itérables
text = "Python"
for char in text:
print(char) # Output: P, y, t, h, o, n (on separate lines)
# Les dictionnaires sont itérables (sur les clés par défaut)
student = {"name": "Alice", "age": 20, "grade": "A"}
for key in student:
print(key) # Output: name, age, grade (on separate lines)Tous ces objets — listes, chaînes de caractères, dictionnaires, tuples, ensembles, plages (ranges) et fichiers — sont itérables parce qu’ils prennent en charge le protocole d’itération (iteration protocol) de Python (un ensemble de règles qui permet à Python de boucler dessus).
35.1.2) Ce qui rend un objet itérable
Pour qu’un objet soit itérable, il doit implémenter une méthode spéciale appelée __iter__(). Cette méthode renvoie un objet itérateur (iterator). Ne vous inquiétez pas encore des détails — nous explorerons les itérateurs dans la section suivante.
Vous pouvez vérifier si un objet est itérable en essayant d’obtenir un itérateur à partir de celui-ci en utilisant la fonction intégrée iter() :
# Tester si des objets sont itérables
numbers = [1, 2, 3]
iterator = iter(numbers) # Fonctionne - les listes sont itérables
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hello"
iterator = iter(text) # Fonctionne - les chaînes sont itérables
print(type(iterator)) # Output: <class 'str_iterator'>
# Essayer avec un objet non itérable
value = 42
try:
iterator = iter(value) # Échoue - les entiers ne sont pas itérables
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'int' object is not iterableLorsque vous appelez iter() sur un objet itérable, Python appelle la méthode __iter__() de l’objet et renvoie un itérateur. Si l’objet ne possède pas cette méthode, vous obtenez une TypeError.
35.1.3) Itérables vs séquences
Il est important de comprendre que tous les itérables ne sont pas des séquences (sequences). Une séquence est un type particulier d’itérable qui prend en charge l’indexation et possède un ordre défini.
# Les séquences prennent en charge l’indexation
my_list = [10, 20, 30]
print(my_list[0]) # Output: 10
my_string = "Python"
print(my_string[2]) # Output: t
# Les ensembles sont itérables mais PAS des séquences (pas d’indexation, pas d’ordre garanti)
my_set = {1, 2, 3}
for item in my_set:
print(item) # Fonctionne - les ensembles sont itérables
# Mais l’indexation ne fonctionne pas
try:
print(my_set[0]) # Échoue - les ensembles ne prennent pas en charge l’indexation
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'set' object is not subscriptableDistinction clé : Toutes les séquences (listes, tuples, chaînes de caractères, plages (ranges)) sont des itérables, mais tous les itérables ne sont pas des séquences. Les ensembles et les dictionnaires sont itérables mais pas des séquences, car ils ne prennent pas en charge l’indexation.
35.1.4) Pourquoi l’itérabilité est importante
Comprendre l’itérabilité vous aide à :
- Savoir sur quoi vous pouvez boucler : n’importe quel itérable fonctionne avec les boucles
for - Comprendre les messages d’erreur : « object is not iterable » signifie que vous ne pouvez pas l’utiliser dans une boucle
for - Utiliser les compréhensions (comprehensions) : les compréhensions de liste, d’ensemble et de dictionnaire fonctionnent avec n’importe quel itérable
- Travailler avec des fonctions intégrées : de nombreuses fonctions intégrées comme
sum(),max(),min()etsorted()acceptent n’importe quel itérable
# Tout ceci fonctionne parce que ces fonctions acceptent des itérables
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # Output: 15
text = "Python"
print(max(text)) # Output: y (highest alphabetically)
# Fonctionne même avec des ensembles
unique_values = {10, 5, 20, 15}
print(sorted(unique_values)) # Output: [5, 10, 15, 20]35.2) Les itérateurs du quotidien en Python (fichiers, plages, dictionnaires, et plus)
35.2.1) Qu’est-ce qu’un itérateur
Un itérateur (iterator) est un objet qui représente un flux de données. Il renvoie une valeur à la fois lorsque vous demandez l’élément suivant. Une fois qu’un itérateur a renvoyé toutes ses valeurs, il est épuisé et ne peut pas être réutilisé.
Considérez un itérateur comme un marque-page dans un livre :
- Il se souvient d’où vous en êtes dans la séquence
- Vous pouvez demander l’élément suivant
- Une fois arrivé à la fin, vous ne pouvez pas revenir en arrière sans créer un nouvel itérateur
La différence clé entre un itérable et un itérateur :
- Un itérable est quelque chose sur lequel vous pouvez itérer (comme une liste)
- Un itérateur est l’objet qui fait l’itération (le mécanisme qui avance dans la liste)
# Une liste est un itérable
numbers = [1, 2, 3]
# Obtenir un itérateur à partir de l’itérable
iterator = iter(numbers)
# L’itérateur est un objet distinct
print(type(numbers)) # Output: <class 'list'>
print(type(iterator)) # Output: <class 'list_iterator'>35.2.2) Les itérateurs dans les boucles for
Lorsque vous écrivez une boucle for, Python crée automatiquement un itérateur en coulisses :
numbers = [10, 20, 30]
# Ce que vous écrivez :
for num in numbers:
print(num)
# Ce que Python fait en interne (conceptuellement) :
# 1. Appeler iter(numbers) pour obtenir un itérateur
# 2. Appeler next() de façon répétée sur l’itérateur
# 3. S’arrêter lorsque l’itérateur lève StopIterationVoici à quoi cela ressemble de manière explicite :
numbers = [10, 20, 30]
# Itération manuelle (ce que for fait automatiquement)
iterator = iter(numbers)
try:
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 20
print(next(iterator)) # Output: 30
print(next(iterator)) # Would raise StopIteration
except StopIteration:
print("No more items") # Output: No more itemsLa boucle for gère automatiquement l’exception StopIteration, c’est pourquoi vous ne la voyez jamais dans du code normal.
35.2.3) Les objets fichier comme itérateurs
Les objets fichier sont d’excellents exemples d’itérateurs. Lorsque vous itérez sur un fichier, il lit une ligne à la fois :
# Créer un fichier d’exemple
with open("students.txt", "w") as file:
file.write("Alice\n")
file.write("Bob\n")
file.write("Charlie\n")
# Lire le fichier ligne par ligne
with open("students.txt", "r") as file:
for line in file:
print(line.strip()) # Output: Alice, Bob, Charlie (on separate lines)Les objets fichier sont à la fois itérables et itérateurs. Ils se renvoient eux-mêmes lorsque vous appelez iter() dessus :
with open("students.txt", "r") as file:
iterator = iter(file)
print(file is iterator) # Output: True (same object)
# Lire les lignes manuellement
print(next(iterator)) # Output: Alice
print(next(iterator)) # Output: Bob
print(next(iterator)) # Output: CharlieC’est efficace en mémoire parce que Python ne charge pas le fichier entier en mémoire — il lit une ligne à la fois, à la demande.
35.2.4) Les objets range comme itérateurs
Les objets range sont des itérables qui génèrent des nombres à la demande :
# Un range est un itérable
numbers = range(1, 4)
print(type(numbers)) # Output: <class 'range'>
# Obtenir un itérateur à partir du range
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'range_iterator'>
# Utiliser l’itérateur
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3Les range sont efficaces en mémoire parce qu’ils ne stockent pas tous les nombres en mémoire — ils calculent chaque nombre lorsqu’il est demandé :
# Ce range représente 1 million de nombres mais utilise très peu de mémoire
large_range = range(1000000)
print(type(large_range)) # Output: <class 'range'>
# Obtenir un itérateur
iterator = iter(large_range)
print(next(iterator)) # Output: 0
print(next(iterator)) # Output: 1
# ... peut continuer pour 1 million de valeurs35.2.5) Itérateurs de dictionnaire
Les dictionnaires fournissent différents itérateurs pour les clés, les valeurs et les éléments :
student = {"name": "Alice", "age": 20, "grade": "A"}
# Itérer sur les clés (par défaut)
for key in student:
print(key) # Output: name, age, grade (on separate lines)
# Obtenir explicitement un itérateur de clés
keys_iterator = iter(student.keys())
print(next(keys_iterator)) # Output: name
print(next(keys_iterator)) # Output: age
# Itérer sur les valeurs
values_iterator = iter(student.values())
print(next(values_iterator)) # Output: Alice
print(next(values_iterator)) # Output: 20
# Itérer sur les éléments (paires clé-valeur)
items_iterator = iter(student.items())
print(next(items_iterator)) # Output: ('name', 'Alice')
print(next(items_iterator)) # Output: ('age', 20)35.2.6) Les itérateurs sont épuisables
Une propriété cruciale des itérateurs est qu’ils ne peuvent être utilisés qu’une seule fois. Une fois épuisés, ils ne se réinitialisent pas :
numbers = [1, 2, 3]
iterator = iter(numbers)
# Premier passage dans l’itérateur
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
# L’itérateur est maintenant épuisé
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("Iterator exhausted") # Output: Iterator exhausted
# Pour itérer à nouveau, créez un nouvel itérateur
iterator = iter(numbers)
print(next(iterator)) # Output: 1 (fresh start)C’est différent de l’itérable lui-même, qui peut être parcouru plusieurs fois :
numbers = [1, 2, 3]
# Première itération
for num in numbers:
print(num) # Output: 1, 2, 3
# Deuxième itération (fonctionne très bien - crée un nouvel itérateur)
for num in numbers:
print(num) # Output: 1, 2, 335.3) Utiliser iter() et next() pour avancer dans les itérables
35.3.1) La fonction iter()
La fonction iter() prend un itérable et renvoie un itérateur. C’est la première étape du protocole d’itération :
# Créer des itérateurs à partir de différents itérables
numbers = [10, 20, 30]
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hi"
text_iterator = iter(text)
print(type(text_iterator)) # Output: <class 'str_iterator'>
my_set = {1, 2, 3}
set_iterator = iter(my_set)
print(type(set_iterator)) # Output: <class 'set_iterator'>Chaque type d’itérable renvoie son propre type d’itérateur spécialisé, mais ils fonctionnent tous de la même manière — vous appelez next() pour obtenir la valeur suivante.
35.3.2) La fonction next()
La fonction next() récupère l’élément suivant à partir d’un itérateur. Lorsqu’il n’y a plus d’éléments, elle lève StopIteration :
colors = ["red", "green", "blue"]
iterator = iter(colors)
# Récupérer les éléments un par un
print(next(iterator)) # Output: red
print(next(iterator)) # Output: green
print(next(iterator)) # Output: blue
# Plus d’éléments
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("No more colors") # Output: No more colors35.3.3) Fournir une valeur par défaut à next()
Vous pouvez fournir une valeur par défaut à next() en second argument. Lorsque l’itérateur est épuisé, au lieu de lever une exception StopIteration, next() renverra la valeur par défaut que vous avez spécifiée :
numbers = [1, 2, 3]
iterator = iter(numbers)
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
print(next(iterator, "Done")) # Output: Done (default value, no exception)
print(next(iterator, "Done")) # Output: Done (still exhausted)C’est utile lorsque vous voulez gérer la fin de l’itération de manière élégante, sans gestion d’exception :
35.4) Créer des itérateurs personnalisés avec iter et next
35.4.1) Pourquoi créer des itérateurs personnalisés
Les itérables intégrés de Python (listes, chaînes de caractères, fichiers) couvrent la plupart des cas courants. Cependant, il arrive que vous ayez besoin de créer vos propres objets itérables pour un comportement spécialisé :
- Générer des séquences avec une logique personnalisée
- Itérer sur des structures de données que vous concevez
- Créer une itération efficace en mémoire sur de grands jeux de données
- Implémenter l’évaluation paresseuse (lazy evaluation) (calculer les valeurs uniquement lorsque nécessaire)
Créer un itérateur personnalisé nécessite d’implémenter deux méthodes spéciales : __iter__() et __next__().
35.4.2) Le protocole de l’itérateur
Pour faire d’un objet un itérateur, il doit implémenter :
__iter__(): renvoie l’objet itérateur lui-même (souventself)__next__(): renvoie la valeur suivante dans la séquence, ou lèveStopIterationlorsque c’est terminé
class SimpleCounter:
"""An iterator that counts from start to end."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
"""Return the iterator object (self)."""
return self
def __next__(self):
"""Return the next value or raise StopIteration."""
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Using the custom iterator
counter = SimpleCounter(1, 5)
for num in counter:
print(num)
# Output: 1
# Output: 2
# Output: 3
# Output: 4
# Output: 5Décomposons ce qui se passe :
- La boucle
forappelleiter(counter), ce qui appellecounter.__iter__()et récupèrecounterlui-même - La boucle appelle
next(counter)de façon répétée, ce qui appellecounter.__next__() - Chaque appel à
__next__()renvoie le nombre suivant et incrémentecurrent - Quand
current > end,__next__()lèveStopIteration, et la boucle s’arrête
35.4.3) Utilisation manuelle d’itérateurs personnalisés
Vous pouvez aussi utiliser des itérateurs personnalisés manuellement avec iter() et next() :
counter = SimpleCounter(10, 13)
# Get the iterator (returns itself)
iterator = iter(counter)
print(iterator is counter) # Output: True
# Get values manually
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 11
print(next(iterator)) # Output: 12
print(next(iterator)) # Output: 13
# Now exhausted
try:
print(next(iterator))
except StopIteration:
print("Counter exhausted") # Output: Counter exhausted35.4.4) Les itérateurs sont épuisables (rappel)
Souvenez-vous que les itérateurs ne peuvent être utilisés qu’une seule fois :
counter = SimpleCounter(1, 3)
# First iteration
for num in counter:
print(num) # Output: 1, 2, 3
# Second iteration (doesn't work - iterator is exhausted)
for num in counter:
print(num) # Nothing printed - iterator is already exhaustedPour itérer à nouveau, vous devez créer une nouvelle instance :
# Create a new counter for each iteration
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3 (new iterator)35.4.5) Créer une classe itérable (pas seulement un itérateur)
Souvent, vous voulez une classe qui soit itérable mais qui crée un nouvel itérateur à chaque fois. Pour cela, séparez l’itérable de l’itérateur :
class CounterIterable:
"""An iterable that creates fresh counter iterators."""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
"""Return a new iterator each time."""
return CounterIterator(self.start, self.end)
class CounterIterator:
"""The actual iterator that does the counting."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Now we can iterate multiple times
counter = CounterIterable(1, 3)
# First iteration
for num in counter:
print(num) # Output: 1, 2, 3
# Second iteration (works because __iter__ creates a new iterator)
for num in counter:
print(num) # Output: 1, 2, 3Ce modèle sépare les responsabilités :
CounterIterableest l’itérable — il sait comment créer des itérateursCounterIteratorest l’itérateur — il sait comment avancer à travers les valeurs
35.4.6) Exemple pratique : itérer sur une structure de données personnalisée
Créons un itérateur pour une structure de données personnalisée — une playlist simple :
class Playlist:
"""A music playlist that can be iterated over."""
def __init__(self):
self.songs = []
def add_song(self, title, artist):
"""Add a song to the playlist."""
self.songs.append({"title": title, "artist": artist})
def __iter__(self):
"""Return an iterator for the playlist."""
return PlaylistIterator(self.songs)
class PlaylistIterator:
"""Iterator for stepping through songs in a playlist."""
def __init__(self, songs):
self.songs = songs
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.songs):
raise StopIteration
song = self.songs[self.index]
self.index += 1
return song
# Using the playlist
playlist = Playlist()
playlist.add_song("Imagine", "John Lennon")
playlist.add_song("Bohemian Rhapsody", "Queen")
playlist.add_song("Hotel California", "Eagles")
# Iterate over songs
print("Now playing:")
for song in playlist:
print(f" {song['title']} by {song['artist']}")
# Output: Now playing:
# Output: Imagine by John Lennon
# Output: Bohemian Rhapsody by Queen
# Output: Hotel California by Eagles
# Can iterate again (creates a new iterator)
print("\nReplay:")
for song in playlist:
print(f" {song['title']}")
# Output: Replay:
# Output: Imagine
# Output: Bohemian Rhapsody
# Output: Hotel California35.4.7) Quand utiliser des itérateurs personnalisés
Créez des itérateurs personnalisés lorsque :
- Vous avez besoin d’évaluation paresseuse (lazy evaluation) : générer des valeurs à la demande plutôt que de toutes les stocker
- Vous avez une structure de données personnalisée : la rendre itérable pour qu’elle fonctionne avec les boucles
for - Vous avez besoin d’une logique d’itération spéciale : sauter des éléments, transformer des valeurs, ou implémenter un avancement complexe
- L’efficacité mémoire compte : générer de grandes séquences sans les stocker
Cependant, au chapitre 36, vous découvrirez les générateurs (generators), qui offrent une manière bien plus simple de créer des itérateurs en utilisant le mot-clé yield. Les générateurs sont généralement préférés à l’implémentation manuelle de __iter__() et __next__() parce qu’ils sont plus concis et plus faciles à comprendre.
Comprendre comment créer des itérateurs personnalisés vous donne un aperçu du fonctionnement du protocole d’itération de Python, même si vous utiliserez souvent les générateurs à la place. Les concepts que vous avez appris ici — __iter__(), __next__(), et StopIteration — sont fondamentaux pour comprendre les générateurs et d’autres techniques d’itération avancées dans le prochain chapitre.