Les itérateurs sont des objets sur lesquels on peut itérer. Il s’agit d’une fonctionnalité commune du langage Python, très utilisée dans les boucles et les compréhensions de listes. Tout objet capable de produire un itérateur est appelé un itérable.
Créer un itérateur demande un certain travail. Par exemple, l’implémentation de chaque objet itérateur doit définir les méthodes __iter__() et __next__(). En plus de ces prérequis, l’implémentation doit suivre l’état interne de l’objet et lever une exception StopIteration lorsqu’il n’y a plus de valeurs à retourner. Ces règles constituent le protocole des itérateurs.
Implémenter son propre itérateur est un processus fastidieux, et ce n’est pas toujours nécessaire. Une alternative plus simple consiste à utiliser un générateur. Les générateurs sont un type particulier de fonction qui utilisent le mot-clé yield pour retourner un itérateur pouvant être parcouru, une valeur à la fois.
Savoir quand implémenter un itérateur et quand utiliser un générateur renforcera vos compétences de développeur Python. Dans la suite de ce tutoriel, nous mettrons en avant les distinctions entre ces deux objets afin de vous aider à choisir la meilleure approche selon les cas d’usage.
Glossaire
Terme
Définition
Itérable
Un objet Python qui peut être parcouru dans une boucle. Exemples : listes, ensembles, tuples, dictionnaires, chaînes de caractères, etc.
Itérateur
Un itérateur est un objet sur lequel on peut itérer. Il contient donc un nombre dénombrable de valeurs.
Générateur
Un type particulier de fonction qui ne renvoie pas une unique valeur : elle renvoie un objet itérateur produisant une séquence de valeurs.
Évaluation paresseuse
Stratégie d’évaluation selon laquelle certains objets ne sont produits que lorsqu’ils sont requis. Dans certains cercles de développeurs, on parle aussi de « call-by-need ».
Protocole des itérateurs
Un ensemble de règles à respecter pour définir un itérateur en Python.
next()
Fonction native qui permet de retourner l’élément suivant d’un itérateur.
iter()
Fonction native qui convertit un itérable en itérateur.
yield()
Mot-clé Python similaire à return, sauf que yield renvoie un objet générateur au lieu d’une valeur.
Itérateurs et itérables en Python
Les itérables sont des objets capables de retourner leurs éléments un par un : on peut donc les parcourir. Les structures de données Python intégrées les plus courantes, comme les listes, tuples et ensembles, sont des itérables. D’autres structures comme les chaînes et les dictionnaires le sont aussi : une chaîne peut être parcourue caractère par caractère et on peut itérer sur les clés d’un dictionnaire. En règle générale, tout objet que l’on peut parcourir avec une boucle for est un itérable.
Explorer les itérables Python avec des exemples
Au vu des définitions, on peut conclure que tous les itérateurs sont aussi des itérables. En revanche, tout itérable n’est pas forcément un itérateur. Un itérable ne produit un itérateur que lorsqu’on l’itère.
Pour le démontrer, nous allons instancier une liste (un itérable) et produire un itérateur en appelant la fonction native iter() sur cette liste.
list_instance =[1,2,3,4]print(iter(list_instance))"""
<list_iterator object at 0x7fd946309e90>
"""Propulsé par
Bien que la liste en elle-même ne soit pas un itérateur, l’appel à la fonction iter() la convertit en itérateur et renvoie l’objet itérateur.
Pour montrer que tous les itérables ne sont pas des itérateurs, nous allons instancier la même liste et tenter d’appeler la fonction next(), qui sert à retourner l’élément suivant d’un itérateur.
list_instance =[1,2,3,4]print(next(list_instance))"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""Propulsé par
Dans le code ci-dessus, tenter d’appeler next() sur la liste déclenche une TypeError – en savoir plus sur la gestion des exceptions et des erreurs en Python. Ce comportement s’explique simplement par le fait qu’une liste est un itérable, pas un itérateur.
Explorer les itérateurs Python avec des exemples
Ainsi, si l’objectif est d’itérer sur une liste, il faut d’abord produire un objet itérateur. C’est seulement ensuite que l’on peut avancer dans les valeurs de la liste.
# instancier une liste
list_instance =[1,2,3,4]# convertir la liste en itérateur
iterator =iter(list_instance)# retourner les éléments un par unprint(next(iterator))print(next(iterator))print(next(iterator))print(next(iterator))"""
1
2
3
4
"""Propulsé par
Python crée automatiquement un objet itérateur lorsque vous parcourez un itérable en boucle.
# instancier une liste
list_instance =[1,2,3,4]# parcourir la listefor item in list_instance:print(item)"""
1
2
3
4
"""Propulsé par
Quand l’exception StopIteration est levée, la boucle se termine.
Les valeurs d’un itérateur ne peuvent être lues que de gauche à droite. Python ne propose pas de fonction previous() permettant de revenir en arrière dans un itérateur.
La nature paresseuse des itérateurs
Il est possible de définir plusieurs itérateurs à partir du même itérable. Chaque itérateur maintient son propre état d’avancement. Ainsi, en définissant plusieurs instances d’itérateurs d’un même itérable, on peut atteindre la fin avec l’une pendant que l’autre reste au début.
Remarquez que iterator_b affiche le premier élément de la série.
On peut donc dire que les itérateurs ont une nature paresseuse : à la création d’un itérateur, les éléments ne sont produits que lorsqu’ils sont demandés. Autrement dit, les éléments de notre liste ne seraient retournés que si nous les demandons explicitement avec next(iter(list_instance)).
Cependant, on peut extraire toutes les valeurs d’un itérateur en une fois en appelant une structure de données itérable standard (p. ex. list(), set(), tuple()) sur l’objet itérateur pour le forcer à générer tous ses éléments d’un coup.
# instancier un itérable
list_instance =[1,2,3,4]# produire un itérateur à partir de l’itérable
iterator =iter(list_instance)print(list(iterator))"""
[1, 2, 3, 4]
"""Propulsé par
Ce n’est pas recommandé pour de grands itérateurs, car cela force la génération et la conservation en mémoire de chaque élément, ce qui va à l’encontre de l’évaluation paresseuse.
Quand un jeu de données est trop volumineux pour tenir confortablement en mémoire, ou lorsque vous souhaitez une itération paresseuse sans écrire une classe d’itérateur complète, un générateur est généralement mieux adapté.
Générateurs Python
L’alternative la plus simple à l’implémentation d’un itérateur est d’utiliser un générateur. Bien que les générateurs ressemblent à des fonctions Python classiques, ils sont différents. Pour commencer, un générateur ne renvoie pas des éléments de façon traditionnelle : il utilise le mot-clé yield pour produire des éléments à la volée. On peut donc dire qu’un générateur est un type particulier de fonction qui s’appuie sur l’évaluation paresseuse.
Les générateurs ne stockent pas leur contenu en mémoire comme on pourrait s’y attendre d’un itérable classique. Par exemple, si l’objectif est de trouver tous les diviseurs d’un entier positif, on écrirait habituellement une fonction traditionnelle (découvrez-en plus sur les fonctions Python dans ce tutoriel) comme suit :
deffactors(n):
factor_list =[]for val inrange(1, n+1):if n % val ==0:
factor_list.append(val)return factor_list
print(factors(20))"""
[1, 2, 4, 5, 10, 20]
"""Propulsé par
Le code ci-dessus renvoie la liste complète des diviseurs. Voyez maintenant la différence lorsque l’on utilise un générateur à la place d’une fonction Python traditionnelle :
deffactors(n):for val inrange(1, n+1):if n % val ==0:yield val
print(factors(20))"""
<generator object factors at 0x7fd938271350>
"""Propulsé par
Puisque nous avons utilisé le mot-clé yield au lieu de return, l’exécution de la fonction ne se termine pas immédiatement. En essence, nous avons demandé à Python de créer un objet générateur plutôt qu’une fonction traditionnelle, ce qui permet de suivre l’état du générateur.
Il devient alors possible d’appeler la fonction next() sur cet itérateur paresseux pour révéler les éléments de la série un par un.
deffactors(n):for val inrange(1, n+1):if n % val ==0:yield val
factors_of_20 = factors(20)print(next(factors_of_20))"""
1
"""Propulsé par
Une autre manière de créer un générateur est d’utiliser une compréhension de générateur. La syntaxe est similaire à celle d’une compréhension de liste, mais avec des parenthèses au lieu des crochets.
factor_gen =(val for val inrange(1,21)if20% val ==0)print(list(factor_gen))"""
[1, 2, 4, 5, 10, 20]
"""Propulsé par
Comprendre le mot-clé yield en Python
Le mot-clé yield contrôle le flux d’une fonction génératrice. Au lieu de quitter la fonction comme avec return, yield renvoie une valeur mais mémorise l’état de ses variables locales.
Le générateur retourné par l’appel à yield peut être affecté à une variable puis parcouru avec la fonction next() – cela exécutera la fonction jusqu’au premier yield rencontré. Une fois le yield atteint, l’exécution de la fonction est suspendue. À ce moment, l’état de la fonction est sauvegardé. Il est donc possible de reprendre l’exécution quand on le souhaite.
La fonction reprendra à partir de l’appel à yield. Par exemple :
defyield_multiple_statements():yield"This is the first statement"yield"This is the second statement"yield"This is the third statement"yield"This is the last statement. Don't call next again!"
example = yield_multiple_statements()print(next(example))print(next(example))print(next(example))print(next(example))print(next(example))"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""Propulsé par
Dans le code ci-dessus, notre générateur comporte quatre appels à yield, mais nous appelons next cinq fois, ce qui lève une exception StopIteration. Ce comportement survient parce que notre générateur n’est pas une série infinie : l’appeler plus de fois qu’il n’y a d’éléments épuise le générateur.
En résumé
Pour récapituler, les itérateurs sont des objets sur lesquels on peut itérer, et les générateurs sont des fonctions particulières qui s’appuient sur l’évaluation paresseuse. Implémenter votre propre itérateur implique de définir les méthodes __iter__() et __next__(), tandis qu’un générateur s’implémente en utilisant le mot-clé yield dans une fonction ou une compréhension Python.
Vous privilégierez un itérateur personnalisé à un générateur si vous avez besoin d’un objet gérant un état complexe ou si vous souhaitez exposer d’autres méthodes au-delà de __next__(), __iter__() et __init__(). À l’inverse, un générateur sera préférable pour traiter de grands volumes de données, puisqu’il ne stocke pas son contenu en mémoire, ou lorsqu’il n’est pas nécessaire d’implémenter un itérateur complet.
Quelle est la différence entre un itérateur et un générateur en Python ?⌄
Un itérateur est tout objet qui implémente __iter__() et __next__(). Un générateur est un moyen plus simple de créer un itérateur via une fonction utilisant le mot-clé yield. Tous les générateurs sont des itérateurs, mais tous les itérateurs ne sont pas des générateurs.
Quand dois-je utiliser un générateur plutôt qu’une liste en Python ?⌄
Utilisez un générateur pour des séquences longues ou infinies, ou lorsque l’efficacité mémoire est essentielle. Les listes conservent tous les éléments en mémoire, tandis que les générateurs produisent une valeur à la fois. Pour de petits jeux de données que vous réutiliserez, une liste convient généralement.
À quoi sert le mot-clé yield en Python ?⌄
Le mot-clé yield transforme une fonction en générateur. Au lieu de retourner puis de quitter, yield met la fonction en pause, renvoie une valeur et mémorise son état pour reprendre à l’appel suivant.
Comment créer un générateur en Python ?⌄
Écrivez soit une fonction qui utilise yield au lieu de return, soit une expression génératrice — même syntaxe qu’une compréhension de liste mais avec des parenthèses, par exemple (x * 2 for x in range(10)).
Les générateurs sont-ils plus rapides que les itérateurs en Python ?
⌄
Pas en vitesse brute, mais ils sont plus économes en mémoire car ils produisent les valeurs à la demande. Pour de grands jeux de données, cela se traduit souvent par de meilleures performances globales ; pour de petits volumes, la différence est négligeable.
Tutoriel complet d'introduction aux boucles Python. Apprenez et pratiquez les boucles while et for, les boucles imbriquées, les mots-clés break et continue, la fonction range et bien plus encore.
Initiez-vous aux structures de données de Python : apprenez-en plus sur les types de données et les structures de données primitives et non primitives, telles que les chaînes de caractères, les listes, les piles, etc.
Découvrez les fonctions et méthodes des listes Python. Veuillez suivre les exemples de code pour list() et d'autres fonctions et méthodes Python dès maintenant.
Les itérateurs sont des objets sur lesquels on peut itérer. Il s’agit d’une fonctionnalité commune du langage Python, très utilisée dans les boucles et les compréhensions de listes. Tout objet capable de produire un itérateur est appelé un itérable.
Créer un itérateur demande un certain travail. Par exemple, l’implémentation de chaque objet itérateur doit définir les méthodes
__iter__()et__next__(). En plus de ces prérequis, l’implémentation doit suivre l’état interne de l’objet et lever une exceptionStopIterationlorsqu’il n’y a plus de valeurs à retourner. Ces règles constituent le protocole des itérateurs.Implémenter son propre itérateur est un processus fastidieux, et ce n’est pas toujours nécessaire. Une alternative plus simple consiste à utiliser un générateur. Les générateurs sont un type particulier de fonction qui utilisent le mot-clé
yieldpour retourner un itérateur pouvant être parcouru, une valeur à la fois.Savoir quand implémenter un itérateur et quand utiliser un générateur renforcera vos compétences de développeur Python. Dans la suite de ce tutoriel, nous mettrons en avant les distinctions entre ces deux objets afin de vous aider à choisir la meilleure approche selon les cas d’usage.
Glossaire
Terme
Définition
Itérable
Un objet Python qui peut être parcouru dans une boucle. Exemples : listes, ensembles, tuples, dictionnaires, chaînes de caractères, etc.
Itérateur
Un itérateur est un objet sur lequel on peut itérer. Il contient donc un nombre dénombrable de valeurs.
Générateur
Un type particulier de fonction qui ne renvoie pas une unique valeur : elle renvoie un objet itérateur produisant une séquence de valeurs.
Évaluation paresseuse
Stratégie d’évaluation selon laquelle certains objets ne sont produits que lorsqu’ils sont requis. Dans certains cercles de développeurs, on parle aussi de « call-by-need ».
Protocole des itérateurs
Un ensemble de règles à respecter pour définir un itérateur en Python.
next()
Fonction native qui permet de retourner l’élément suivant d’un itérateur.
iter()
Fonction native qui convertit un itérable en itérateur.
yield()
Mot-clé Python similaire à return, sauf que
yieldrenvoie un objet générateur au lieu d’une valeur.Itérateurs et itérables en Python
Les itérables sont des objets capables de retourner leurs éléments un par un : on peut donc les parcourir. Les structures de données Python intégrées les plus courantes, comme les listes, tuples et ensembles, sont des itérables. D’autres structures comme les chaînes et les dictionnaires le sont aussi : une chaîne peut être parcourue caractère par caractère et on peut itérer sur les clés d’un dictionnaire. En règle générale, tout objet que l’on peut parcourir avec une boucle for est un itérable.
Explorer les itérables Python avec des exemples
Au vu des définitions, on peut conclure que tous les itérateurs sont aussi des itérables. En revanche, tout itérable n’est pas forcément un itérateur. Un itérable ne produit un itérateur que lorsqu’on l’itère.
Pour le démontrer, nous allons instancier une liste (un itérable) et produire un itérateur en appelant la fonction native
iter()sur cette liste.Bien que la liste en elle-même ne soit pas un itérateur, l’appel à la fonction
iter()la convertit en itérateur et renvoie l’objet itérateur.Pour montrer que tous les itérables ne sont pas des itérateurs, nous allons instancier la même liste et tenter d’appeler la fonction
next(), qui sert à retourner l’élément suivant d’un itérateur.Dans le code ci-dessus, tenter d’appeler
next()sur la liste déclenche uneTypeError– en savoir plus sur la gestion des exceptions et des erreurs en Python. Ce comportement s’explique simplement par le fait qu’une liste est un itérable, pas un itérateur.Explorer les itérateurs Python avec des exemples
Ainsi, si l’objectif est d’itérer sur une liste, il faut d’abord produire un objet itérateur. C’est seulement ensuite que l’on peut avancer dans les valeurs de la liste.
Python crée automatiquement un objet itérateur lorsque vous parcourez un itérable en boucle.
Quand l’exception
StopIterationest levée, la boucle se termine.Les valeurs d’un itérateur ne peuvent être lues que de gauche à droite. Python ne propose pas de fonction
previous()permettant de revenir en arrière dans un itérateur.La nature paresseuse des itérateurs
Il est possible de définir plusieurs itérateurs à partir du même itérable. Chaque itérateur maintient son propre état d’avancement. Ainsi, en définissant plusieurs instances d’itérateurs d’un même itérable, on peut atteindre la fin avec l’une pendant que l’autre reste au début.
Remarquez que
iterator_baffiche le premier élément de la série.On peut donc dire que les itérateurs ont une nature paresseuse : à la création d’un itérateur, les éléments ne sont produits que lorsqu’ils sont demandés. Autrement dit, les éléments de notre liste ne seraient retournés que si nous les demandons explicitement avec
next(iter(list_instance)).Cependant, on peut extraire toutes les valeurs d’un itérateur en une fois en appelant une structure de données itérable standard (p. ex.
list(),set(),tuple()) sur l’objet itérateur pour le forcer à générer tous ses éléments d’un coup.Ce n’est pas recommandé pour de grands itérateurs, car cela force la génération et la conservation en mémoire de chaque élément, ce qui va à l’encontre de l’évaluation paresseuse.
Quand un jeu de données est trop volumineux pour tenir confortablement en mémoire, ou lorsque vous souhaitez une itération paresseuse sans écrire une classe d’itérateur complète, un générateur est généralement mieux adapté.
Générateurs Python
L’alternative la plus simple à l’implémentation d’un itérateur est d’utiliser un générateur. Bien que les générateurs ressemblent à des fonctions Python classiques, ils sont différents. Pour commencer, un générateur ne renvoie pas des éléments de façon traditionnelle : il utilise le mot-clé
yieldpour produire des éléments à la volée. On peut donc dire qu’un générateur est un type particulier de fonction qui s’appuie sur l’évaluation paresseuse.Les générateurs ne stockent pas leur contenu en mémoire comme on pourrait s’y attendre d’un itérable classique. Par exemple, si l’objectif est de trouver tous les diviseurs d’un entier positif, on écrirait habituellement une fonction traditionnelle (découvrez-en plus sur les fonctions Python dans ce tutoriel) comme suit :
Le code ci-dessus renvoie la liste complète des diviseurs. Voyez maintenant la différence lorsque l’on utilise un générateur à la place d’une fonction Python traditionnelle :
Puisque nous avons utilisé le mot-clé
yieldau lieu dereturn, l’exécution de la fonction ne se termine pas immédiatement. En essence, nous avons demandé à Python de créer un objet générateur plutôt qu’une fonction traditionnelle, ce qui permet de suivre l’état du générateur.Il devient alors possible d’appeler la fonction
next()sur cet itérateur paresseux pour révéler les éléments de la série un par un.Une autre manière de créer un générateur est d’utiliser une compréhension de générateur. La syntaxe est similaire à celle d’une compréhension de liste, mais avec des parenthèses au lieu des crochets.
Comprendre le mot-clé yield en Python
Le mot-clé
yieldcontrôle le flux d’une fonction génératrice. Au lieu de quitter la fonction comme avecreturn,yieldrenvoie une valeur mais mémorise l’état de ses variables locales.Le générateur retourné par l’appel à
yieldpeut être affecté à une variable puis parcouru avec la fonctionnext()– cela exécutera la fonction jusqu’au premieryieldrencontré. Une fois leyieldatteint, l’exécution de la fonction est suspendue. À ce moment, l’état de la fonction est sauvegardé. Il est donc possible de reprendre l’exécution quand on le souhaite.La fonction reprendra à partir de l’appel à
yield. Par exemple :Dans le code ci-dessus, notre générateur comporte quatre appels à
yield, mais nous appelonsnextcinq fois, ce qui lève une exceptionStopIteration. Ce comportement survient parce que notre générateur n’est pas une série infinie : l’appeler plus de fois qu’il n’y a d’éléments épuise le générateur.En résumé
Pour récapituler, les itérateurs sont des objets sur lesquels on peut itérer, et les générateurs sont des fonctions particulières qui s’appuient sur l’évaluation paresseuse. Implémenter votre propre itérateur implique de définir les méthodes
__iter__()et__next__(), tandis qu’un générateur s’implémente en utilisant le mot-clé yield dans une fonction ou une compréhension Python.Vous privilégierez un itérateur personnalisé à un générateur si vous avez besoin d’un objet gérant un état complexe ou si vous souhaitez exposer d’autres méthodes au-delà de
__next__(),__iter__()et__init__(). À l’inverse, un générateur sera préférable pour traiter de grands volumes de données, puisqu’il ne stocke pas son contenu en mémoire, ou lorsqu’il n’est pas nécessaire d’implémenter un itérateur complet.