I. Introduction▲
Les itérateurs et les générateurs sont les concepts qui se cachent derrière la possibilité de parcourir une liste dans une boucle for, les caractères d'une chaîne de caractères, etc.
Si l'on peut se contenter de se servir des fonctionnalités de base, il arrive toujours un moment dans la vie du développeur où il devra passer au stade supérieur et se plonger dans le fonctionnement de ces fonctionnalités.
Je vous propose ici de passer ce stade et d'approfondir la compréhension du fonctionnement des itérateurs et générateurs.
II. Les itérateurs▲
II-A. Présentation▲
Un itérateur est un objet permettant de parcourir tout autre objet dit « itérable ». Les exemples les plus connus d'objets itérables sont les chaines de caractères, les listes, les fichiers… Bref tout objet que l'on peut parcourir via un index (appelés séquences : cf. documentation officielle).
Les itérateurs sont très présents dans Python, et toute personne ayant codé un minimum en Python les a déjà rencontrés.
Le plus grand avantage des itérateurs est leur faible consommation ressource.
II-B. La fonction iter() et la méthode next()▲
2.
3.
chaine =
'hello world'
for
lettre in
chaine:
print
(
lettre)
Voici un exemple typique de code s'appuyant sur un itérateur. Dans cet exemple, la boucle FOR récupère l'itérateur de la liste et l'utilise afin de parcourir la liste.
Python possède une fonction faisant partie des Built-In (fonctions de base) nommée « iter() ». Cette fonction permet de créer un itérateur sur un objet itérable.
Dans notre cas, je vous invite à saisir le code suivant.
2.
3.
chaine =
'hello world'
iterateur =
iter(
chaine)
print
(
iterateur)
Certains objets tels les listes possèdent déjà leur propre itérateur accessible via __iter__(). On obtient alors un itérateur plus spécifique. Le code vu à l'instant peut ainsi s'écrire :
2.
3.
chaine =
['h'
,'e'
,'l'
,'l'
,'o'
,' '
,'w'
,'o'
,'r'
,'l'
,'d'
]
iterateur =
chaine.__iter__
(
)
print
(
iterateur)
Vous voyez ici que vous avez bien récupéré un objet de type « iterator ». L'ensemble des objets de type « iterator » possède une fonction « next() », qui permet de se déplacer dans l'objet itérable.
Reprenons notre code initial, en le modifiant légèrement.
Si vous exécutez ce code, vous vous rendrez compte que le résultat est identique au code initial. Que se passe-t-il donc dans ce code ? Et à quoi correspond la méthode next() ?
Tout d'abord, nous créons un itérateur sur notre chaîne de caractères. Dans la boucle FOR, le seul lien que nous prenons en compte est la longueur de cette chaîne de caractères.
Au niveau du print, nous utilisons une fonctionnalité des itérateurs : la méthode « next() ». Cette méthode permet de déplacer l'index de l'itérateur sur l'objet.
À la création de l'itérateur, cet index peut être considéré comme valant « -1 ». L'appel à la méthode « next() » déplace le curseur d'une unité, puis renvoie la valeur lue à cet index.
II-C. Créons notre propre itérateur▲
Maintenant que nous avons vu les bases des itérateurs, je vous propose de créer notre propre itérateur afin de comprendre, entre autres, comment ce dernier sait quand s'arrêter.
Répondons déjà à cette dernière interrogation. Lorsque nous avons atteint le dernier élément, un nouvel appel à « next() » provoquera simplement une exception de type « StopIteration ». Il suffit alors d'intercepter cette exception pour arrêter l'itération.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
class
MonIterateur
(
object):
def
__init__
(
self, obj):
self.obj =
obj
self.length =
len(
obj)
self.count =
0
def
__iter__
(
self):
return
self
def
next
(
self):
if
self.count >
self.length:
raise
StopIteration
else
:
result =
self.obj[self.count]
self.count +=
2
return
result
if
__name__
==
"__main__"
:
chaine =
"hello_world"
ma_classe_iterateur =
MonIterateur
(
chaine)
iterateur =
ma_classe_iterateur.__iter__
(
)
try
:
for
idx in
range(
len(
chaine)):
print
(
iterateur.next
(
))
except
StopIteration
:
print
(
"fin d'iteration"
)
Cet exemple montre comment arrêter une itération. Outre cela, nous pouvons également voir qu'au sein de la fonction « next » de notre itérateur maison, nous avons choisi d'utiliser un pas de deux caractères.
De même, nous aurions pu effectuer un traitement sur la donnée lue avant de la renvoyer.
Enfin, notez que l'on peut simplifier le code du « if __name__... » avec les lignes suivantes.
2.
3.
4.
5.
6.
7.
8.
if
__name__
==
"__main__"
:
chaine =
"hello_world"
ma_classe_iterateur =
MonIterateur
(
chaine)
try
:
for
lettre in
ma_classe_iterateur:
print
(
lettre)
except
StopIteration
:
print
(
"fin d'iteration"
)
III. Les générateurs▲
III-A. Présentation▲
Les générateurs sont des objets Python permettant de créer et de manipuler plus aisément les itérateurs, en plaçant une couche d'abstraction supplémentaire au niveau code.
Il existe deux façons de créer des générateurs. Ils consomment peu de ressources mémoire. Cette faible consommation est due au fait que lorsqu'une valeur est demandée au générateur, il va la « générer », puis simplement la retourner, sans rien conserver en mémoire.
Ils ont été introduits par la PEP 255 et se basent sur l'utilisation du mot clé « yield ».
III-B. Le mot clé « yield »▲
Le mot clé « yield » est à la base des générateurs. C'est grâce à lui qu'un générateur peut fonctionner.
Il peut être assimilé à un « return », à deux principales différences près. Alors qu'un return vous renverra un ensemble de valeurs (sous forme de liste, chaine de caractères…) et mettra fin à l'exécution de la procédure/fonction/..., un yield vous retournera une valeur, puis se mettra en pause, en attendant l'appel suivant.
Je vous invite à exécuter le code suivant afin de mieux comprendre.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
def
mon_generateur
(
):
yield
'h'
yield
'e'
yield
'l'
yield
'l'
yield
'o'
yield
' '
yield
'w'
yield
'o'
yield
'r'
yield
'l'
yield
'd'
if
__name__
==
(
'__main__'
):
generateur =
mon_generateur
(
)
print
generateur
for
value in
generateur:
print
(
value)
Que constatons-nous ? Tout d'abord generateur est bien un objet de type « generator ». Ensuite, au niveau du for, nous appelons une première fois notre objet generateur.
Rappelons que l'objet generateur fonctionne selon notre générateur défini dans « mon_generateur ».
Au niveau de la boucle for, au premier appel, il exécute la première ligne, qu'il faut ici interpréter comme « retourne la valeur h », puis le générateur se met en pause. Au second appel, il se réveille de sa position et exécute la ligne suivante, l'équivalent d'un « retourne la valeur e » puis se met en pause. Et ainsi de suite jusqu'à la fin.
Cela est simplifiable de la manière suivante :
2.
3.
4.
5.
6.
7.
8.
9.
def
mon_generateur
(
data):
for
value in
data:
yield
value
if
__name__
==
(
'__main__'
):
generateur =
mon_generateur
(
"hello world"
)
print
generateur
for
value in
generateur:
print
(
value)
Maintenant, faisons la liaison avec les itérateurs.
Ce code réalise la même chose que précédemment, mais en mettant explicitement en vue l'itérateur.
Avouez tout de même que les générateurs permettent de simplifier l'écriture du code. Qui plus est, étant un niveau plus bas dans le fonctionnement du code, nous pourrions réaliser des traitements plus en amont (exemple ici : remplacer les lettres L par le chiffre 1).
À tout moment, vous pouvez interrompre le générateur via la méthode « close() ».
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
def
mon_generateur
(
data):
for
value in
data:
yield
value
if
__name__
==
(
'__main__'
):
generateur =
mon_generateur
(
"hello world"
)
print
generateur
for
value in
generateur:
if
value ==
'w'
:
generateur.close
(
)
else
:
print
(
value)
IV. Le module itertools▲
Maintenant que nous avons vu itérateurs et générateurs, il faut mettre en évidence ce qui constitue à la fois leur principal avantage et principal défaut : rien n'est gardé en mémoire, tout est volatil.
De fait, il peut s'avérer difficile dans certains cas de prendre conscience de l'ampleur du travail à effectuer et de savoir comment l'optimiser proprement.
C'est là qu'intervient le module itertools.
IV-A. Présentation▲
Comme son nom l'indique, le module itertools fournit un ensemble d'outils pour itérateurs. Plus précisément, il met à disposition un ensemble de fonctionnalités permettant de simplifier tout un ensemble d'opérations, comme une double boucle for imbriquée.
Il est natif en Python et est disponible du moment que Python l'est.
IV-B. Possibilités offertes▲
IV-B-1. Itérateurs infinis▲
Itérateurs |
Arguments |
Description |
Exemple applicatif |
count() |
start [,step] |
Compte à partir de start, par pas de « step » (par défaut à 1) |
Sélectionnez 1. 2. 3. 4. 5. 6.
|
cycle() |
p |
Boucle indéfiniment sur p |
Sélectionnez 1. 2. 3. 4. 5. 6. 7. 8. 9.
|
repeat() |
element [, n] |
Répète n fois l'élément |
Sélectionnez 1. 2. 3. 4. 5. 6. 7.
|
IV-B-2. Itérateurs finis▲
Itérateurs |
Arguments |
Description |
Exemple applicatif |
chain() |
p, q, ... |
Enchaîne les différentes séquences les unes à la suite des autres |
Sélectionnez 1. 2. 3. 4. 5. 6. 7. 8. 9.
|
compress() |
data, selectors |
Permet d'effectuer un filtrage. Plus précisément, cette fonction effectue un ET logique entre les deux listes. Pour rappel, en Python, est considérée comme « vrai » toute valeur différente de 0 et de False. |
Sélectionnez 1. 2. 3. 4. 5. 6. 7.
|
dropwhile() |
pred, seq |
Ne renvoie rien tant que le prédicat est vrai |
Sélectionnez 1. 2. 3. 4. 5. 6.
|
ifilter() |
pred, seq |
Renvoie des éléments vérifiant le prédicat |
Sélectionnez 1. 2. 3. 4. 5. 6. 7. 8.
|
ifilterfalse() |
pred, seq |
Renvoie des éléments ne vérifiant pas le prédicat |
Sélectionnez 1. 2. 3. 4. 5. 6. 7. 8.
|
islice() |
seq, [start,] stop [, step] |
Renvoie les données respectant les instructions start, stop, step fournies |
Sélectionnez 1. 2. 3. 4. 5. 6. 7. 8. 9.
|
imap() |
func, p, q |
Effectue un func(px,qx) |
Sélectionnez 1. 2. 3. 4. 5. 6. 7.
|
takewhile() |
pred, seq |
Renvoie des éléments tant que le prédicat est vérifié |
Sélectionnez 1. 2. 3. 4. 5. 6.
|
izip() |
p, q, ... |
Renvoie des tuples (px,qx) tant que des paires peuvent être formées |
Sélectionnez 1. 2. 3. 4. 5. 6.
|
izip_longest() |
p, q, …, fillvalue='-' |
Renvoie des tuples (px,qx) en utilisant une valeur de substitution en cas de besoin. |
Sélectionnez 1. 2. 3. 4. 5. 6.
|
IV-B-3. Générateurs combinatoires▲
Itérateurs |
Arguments |
Description |
Exemple applicatif |
product() |
p, q, ...[repeat=1] |
Permet de générer des combinaisons, par exemple entre deux listes, remplaçant ainsi une double boucle FOR |
Sélectionnez 1. 2. 3. 4. 5. 6. 7.
|
permutations() |
p[, r] |
Permet de générer des tuples de longueur r, sans répétition |
Sélectionnez 1. 2. 3. 4.
|
combinations() |
p, r |
Permet de générer des tuples de longueur r, triés, sans répétition |
Sélectionnez 1. 2. 3. 4.
|
De prime abord, les fonctions permutations() et combinations() peuvent sembler similaires. Cependant, l'exécution de l'exemple suivant vous donnera un meilleur aperçu des différences.
2.
3.
4.
5.
6.
7.
8.
9.
from
itertools import
*
for
value in
permutations
(
'ABCD'
, 2
):
print
(
value)
print
(
"**********************************"
)
for
value in
combinations
(
'ABCD'
, 2
):
print
(
value)
Vous constaterez que permutations() vous renvoie l'ensemble des permutations possibles, en tenant compte de l'ordre (ex. : ('A','B') et ('B','A') seront tous deux renvoyés), alors que combinations() ne tiendra pas compte de l'ordre (ex. : ('A','B') et ('B','A') seront considérés comme équivalents et seul l'un d'eux sera renvoyé).
IV-C. Le cas product()▲
« product() » fait partie des fonctions que vous verrez souvent. En effet, il permet de remplacer avantageusement une double boucle for imbriquée.
Pour mieux comprendre l'intérêt d'utiliser le module itertools, je vous propose d'exécuter le code suivant.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
from
itertools import
*
import
time
list_01 =
['A'
, 'B'
, 'C'
, 'D'
]
list_02 =
['E'
, 'F'
, 'G'
, 'H'
]
start_01 =
time.time
(
)
for
value in
product
(
list_01, list_02):
print
(
value)
stop_01 =
time.time
(
)
start_02 =
time.time
(
)
for
value_01 in
list_01:
for
value_02 in
list_02:
print
(
value_01, value_02)
stop_02 =
time.time
(
)
print
(
stop_01-
start_01)
print
(
stop_02-
start_02)
Comme on peut le constater, le code avec itertools.product() est plus rapide d'environ 20 %. À l'échelle de notre exemple, cela est négligeable. Mais dans des traitements de masse, cela peut changer énormément les choses. Sans parler du gain en lisibilité et en simplicité.
V. Conclusion▲
Comme nous venons de le voir ensemble, le fonctionnement des itérateurs et des générateurs est élémentaire, et n'a rien de complexe.
En revanche, si un débutant peut très bien se passer de les connaître, ce n'est pas le cas des développeurs confirmés.
En effet, ce concept Python pourra leur permettre d'optimiser profondément leur code et s'avérer indispensable dans certains cas.
Côté module, itertools est un grand classique dont il faut au minimum connaître le nom et les possibilités. D'autant plus que dans certains algorithmes, il se révélera vite incontournable. Sans compter que, comme vu il y a peu, le code n'en devient que plus lisible.
J'espère que ce petit tutoriel vous permettra désormais d'améliorer votre code et d'appréhender plus facilement certaines erreurs que vous pourriez commettre.