Cours
Les itérateurs sont des objets sur lesquels on peut itérer. Ils servent de caractéristique commune au langage de programmation Python, bien rangés pour les boucles et les compréhensions de listes. Tout objet pouvant dériver un itérateur est appelé itérable.
La construction d'un itérateur nécessite beaucoup de travail. Par exemple, l'implémentation de chaque objet itérateur doit consister en une méthode __iter__() et __next__() . Outre les conditions préalables susmentionnées, l'implémentation doit également permettre de suivre l'état interne de l'objet et de lever une exception StopIteration lorsque plus aucune valeur ne peut être renvoyée. Ces règles sont connues sous le nom de protocole de l'itérateur.
La mise en œuvre de votre propre itérateur est un processus fastidieux, qui n'est nécessaire que dans certains cas. Une alternative plus simple consiste à utiliser un objet générateur. Les générateurs sont un type spécial de fonction qui utilise le mot-clé yield pour renvoyer un itérateur qui peut être parcouru, une valeur à la fois.
La capacité à discerner les scénarios appropriés pour mettre en œuvre un itérateur ou utiliser un générateur améliorera vos compétences en tant que programmeur Python. Dans la suite de ce tutoriel, nous mettrons l'accent sur les distinctions entre les deux objets, ce qui vous aidera à choisir celui qui convient le mieux à votre situation.
Glossaire
|
Durée |
Définition |
|
Itérable |
Un objet Python qui peut être parcouru en boucle ou itéré dans une boucle. Les listes, les ensembles, les tuples, les dictionnaires, les chaînes de caractères, etc. sont des exemples d'itérables. |
|
Itérateur |
Un itérateur est un objet sur lequel on peut itérer. Les itérateurs contiennent donc un nombre dénombrable de valeurs. |
|
Générateur |
Un type spécial de fonction qui ne renvoie pas une seule valeur : elle renvoie un objet itérateur avec une séquence de valeurs. |
|
Évaluation paresseuse |
Stratégie d'évaluation selon laquelle certains objets ne sont produits qu'en cas de besoin. C'est pourquoi certains cercles de développeurs font également référence à l'évaluation paresseuse comme "call-by-need" (appel par besoin). |
|
Protocole de l'itérateur |
Ensemble de règles à respecter pour définir un itérateur en Python. |
|
suivant() |
Fonction intégrée utilisée pour renvoyer l'élément suivant d'un itérateur. |
|
iter() |
Fonction intégrée utilisée pour convertir un itérable en itérateur. |
|
yield() |
Mot-clé de Python similaire au mot-clé return, sauf que yield renvoie un objet générateur au lieu d'une valeur. |
Iterators et Iterables en Python
Les itérables sont des objets capables de renvoyer leurs membres un par un - ils peuvent être itérés. Les structures de données intégrées populaires de Python, telles que les listes, les tuples et les ensembles, sont considérées comme des itérables. D'autres structures de données telles que les chaînes et les dictionnaires sont également considérées comme des itérables : une chaîne peut produire une itération de ses caractères, et les clés d'un dictionnaire peuvent être itérées. En règle générale, tout objet sur lequel on peut itérer dans une boucle for est considéré comme un objet itérable.
Explorer les itérables de Python à l'aide d'exemples
Compte tenu des définitions, nous pouvons conclure que tous les itérateurs sont également itérables. Cependant, tout itérable n'est pas nécessairement un itérateur. Un itérable ne produit un itérateur qu'une fois qu'il est itéré.
Pour démontrer cette fonctionnalité, nous allons instancier une liste, qui est un itérable, et produire un itérateur en appelant la fonction intégrée iter() sur la liste.
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
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 démontrer que tous les itérables ne sont pas des itérateurs, nous allons instancier le même objet liste et tenter d'appeler la fonction next(), qui est utilisée pour renvoyer l'élément suivant dans 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
"""
Dans le code ci-dessus, vous pouvez voir que la tentative d'appel de la fonction next() sur la liste a soulevé une TypeError - en savoir plus sur la gestion des exceptions et des erreurs en Python. Ce comportement est dû au simple fait qu'un objet liste est un itérable et non un itérateur.
Explorer les itérateurs Python à l'aide d'exemples
Ainsi, si l'objectif est d'itérer sur une liste, un objet itérateur doit d'abord être produit. Ce n'est qu'ensuite que nous pouvons gérer l'itération à travers les valeurs de la liste.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
Python produit automatiquement un objet itérateur chaque fois que vous tentez de boucler sur un objet itérable.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for iterator in list_instance:
print(iterator)
"""
1
2
3
4
"""
Lorsque l'exception StopIteration est levée, la boucle se termine.
Les valeurs obtenues à partir d'un itérateur ne peuvent être récupérées que de gauche à droite. Python ne dispose pas d'une fonction previous() permettant aux développeurs de revenir en arrière dans un itérateur.
La nature paresseuse des itérateurs
Il est possible de définir plusieurs itérateurs basés sur le même objet itérable. Chaque itérateur conservera son propre état d'avancement. Ainsi, en définissant plusieurs instances d'itérateur d'un objet itérable, il est possible d'itérer jusqu'à la fin d'une instance tandis que l'autre instance reste au début.
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
Remarquez que iterator_b imprime le premier élément de la série.
On peut donc dire que les itérateurs sont paresseux : lorsqu'un itérateur est créé, les éléments ne sont pas cédés tant qu'ils ne sont pas demandés. En d'autres termes, les éléments de notre instance de liste ne seront renvoyés que lorsque nous leur demanderons explicitement de l'être avec next(iter(list_instance)).
Cependant, toutes les valeurs d'un itérateur peuvent être extraites en une seule fois en appelant un conteneur de structure de données itérable intégré (c'est-à-dire list(), set(), tuple()) sur l'objet itérateur pour forcer l'itérateur à générer tous ses éléments en une seule fois.
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
Il n'est pas recommandé d'effectuer cette action, en particulier lorsque les éléments renvoyés par l'itérateur sont volumineux, car le traitement prendra beaucoup de temps.
Lorsqu'un fichier de données volumineux encombre la mémoire de votre machine ou que vous disposez d'une fonction dont l'état interne doit être conservé à chaque appel, mais que la création d'un itérateur n'a pas de sens compte tenu des circonstances, il est préférable d'utiliser un objet générateur.
Générateurs Python
La solution la plus rapide pour mettre en œuvre un itérateur est d'utiliser un générateur. Bien que les générateurs ressemblent à des fonctions Python ordinaires, ils sont différents. Tout d'abord, un objet générateur ne renvoie pas d'éléments. Au lieu de cela, il utilise le mot-clé yield pour générer des articles à la volée. On peut donc dire qu'un générateur est un type particulier de fonction qui tire parti de l'évaluation paresseuse.
Les générateurs ne stockent pas leur contenu en mémoire, contrairement à ce que l'on attendrait d'un itérable classique. Par exemple, si l'objectif était de trouver tous les facteurs d'un entier positif, nous mettrions typiquement en œuvre une fonction traditionnelle (apprenez-en plus sur les fonctions Python dans ce tutoriel) comme suit :
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
Le code ci-dessus renvoie la liste complète des facteurs. Notez toutefois la différence lorsqu'un générateur est utilisé à la place d'une fonction Python traditionnelle :
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
Comme nous avons utilisé le mot-clé yield au lieu de return, la fonction n'est pas quittée après l'exécution. En substance, nous avons demandé à Python de créer un objet générateur au lieu d'une fonction traditionnelle, ce qui permet de suivre l'état de l'objet générateur.
Par conséquent, il est possible d'appeler la fonction next() sur l'itérateur paresseux pour afficher les éléments de la série un par un.
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
Une autre façon de créer un générateur est de le comprendre. Les expressions génératrices adoptent une syntaxe similaire à celle de la compréhension d'une liste, sauf qu'elles utilisent des parenthèses arrondies au lieu de parenthèses carrées.
print((val for val in range(1, 20+1) if n % val == 0))
"""
<generator object <genexpr> at 0x7fd940c31e50>
"""
Explorer les possibilités de Python yield Keyword
Le mot-clé yield contrôle le flux d'une fonction de générateur. Au lieu de sortir de la fonction comme c'est le cas lorsque return est utilisé, le mot-clé yield renvoie la fonction mais garde en mémoire l'état de ses variables locales.
Le générateur renvoyé par l'appel à yield peut être assigné à une variable et itéré avec le mot-clé next() - cela exécutera la fonction jusqu'au premier mot-clé yield qu'elle rencontrera. Lorsque le mot-clé yield est frappé, l'exécution de la fonction est suspendue. Dans ce cas, l'état de la fonction est sauvegardé. Il nous est donc possible de reprendre l'exécution de la fonction à notre guise.
La fonction se poursuit à partir de l'appel à yield. Par exemple :
def yield_multiple_statments():
yield "This is the first statment"
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_statments()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statment
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:
"""
Dans le code ci-dessus, notre générateur a quatre appels à yield, mais nous essayons d'appeler next cinq fois, ce qui soulève une exception StopIteration. Ce comportement s'explique par le fait que notre générateur n'est pas une série infinie, et que le fait de l'appeler plus de fois que prévu a épuisé le générateur.
Synthèse
Pour résumer, les itérateurs sont des objets sur lesquels on peut itérer, et les générateurs sont des fonctions spéciales qui tirent parti de l'évaluation paresseuse. L'implémentation de votre propre itérateur signifie que vous devez créer une méthode __iter__() et __next__(), alors qu'un générateur peut être implémenté en utilisant le mot-clé yield dans une fonction ou une compréhension Python.
Vous préférerez peut-être utiliser un itérateur personnalisé plutôt qu'un générateur si vous avez besoin d'un objet avec un comportement complexe de maintien d'état ou si vous souhaitez exposer d'autres méthodes que __next__(), __iter__(), et __init__(). D'autre part, un générateur peut être préférable lorsqu'il s'agit de traiter de grands ensembles de données car ils ne stockent pas leur contenu en mémoire ou lorsqu'il n'est pas nécessaire d'implémenter un itérateur.