Python & AI Tutorials Logo
Programmation Python

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 :

python
# 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() :

python
# 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 iterable

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

python
# 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 subscriptable

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

Objets Python

Itérables

Non-itérables

Séquences

Itérables non séquentiels

Listes, Tuples, Chaînes, Plages

Ensembles, Dictionnaires, Fichiers

Nombres, None, Booléens

35.1.4) Pourquoi l’itérabilité est importante

Comprendre l’itérabilité vous aide à :

  1. Savoir sur quoi vous pouvez boucler : n’importe quel itérable fonctionne avec les boucles for
  2. Comprendre les messages d’erreur : « object is not iterable » signifie que vous ne pouvez pas l’utiliser dans une boucle for
  3. Utiliser les compréhensions (comprehensions) : les compréhensions de liste, d’ensemble et de dictionnaire fonctionnent avec n’importe quel itérable
  4. Travailler avec des fonctions intégrées : de nombreuses fonctions intégrées comme sum(), max(), min() et sorted() acceptent n’importe quel itérable
python
# 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)
python
# 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 :

python
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 StopIteration

Voici à quoi cela ressemble de manière explicite :

python
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 items

La boucle for gère automatiquement l’exception StopIteration, c’est pourquoi vous ne la voyez jamais dans du code normal.

Oui

Non -> StopIteration

for item in iterable:

Python appelle iter(iterable)

Obtient un objet itérateur

Python appelle next(iterator)

Encore des éléments ?

Assigner à item

Exécuter le corps de la boucle

Quitter la boucle

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 :

python
# 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 :

python
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: Charlie

C’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 :

python
# 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: 3

Les 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é :

python
# 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 valeurs

35.2.5) Itérateurs de dictionnaire

Les dictionnaires fournissent différents itérateurs pour les clés, les valeurs et les éléments :

python
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 :

python
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 :

python
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, 3

35.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 :

python
# 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 :

python
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 colors

35.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 :

python
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 :

  1. __iter__() : renvoie l’objet itérateur lui-même (souvent self)
  2. __next__() : renvoie la valeur suivante dans la séquence, ou lève StopIteration lorsque c’est terminé
python
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: 5

Décomposons ce qui se passe :

  1. La boucle for appelle iter(counter), ce qui appelle counter.__iter__() et récupère counter lui-même
  2. La boucle appelle next(counter) de façon répétée, ce qui appelle counter.__next__()
  3. Chaque appel à __next__() renvoie le nombre suivant et incrémente current
  4. Quand current > end, __next__() lève StopIteration, 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() :

python
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 exhausted

35.4.4) Les itérateurs sont épuisables (rappel)

Souvenez-vous que les itérateurs ne peuvent être utilisés qu’une seule fois :

python
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 exhausted

Pour itérer à nouveau, vous devez créer une nouvelle instance :

python
# 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 :

python
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, 3

Ce modèle sépare les responsabilités :

  • CounterIterable est l’itérable — il sait comment créer des itérateurs
  • CounterIterator est 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 :

python
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 California

35.4.7) Quand utiliser des itérateurs personnalisés

Créez des itérateurs personnalisés lorsque :

  1. Vous avez besoin d’évaluation paresseuse (lazy evaluation) : générer des valeurs à la demande plutôt que de toutes les stocker
  2. Vous avez une structure de données personnalisée : la rendre itérable pour qu’elle fonctionne avec les boucles for
  3. Vous avez besoin d’une logique d’itération spéciale : sauter des éléments, transformer des valeurs, ou implémenter un avancement complexe
  4. 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.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai