On connait la boucle for, qui permet de parcourir les différents éléments d’une liste, d’un tuple, d’une chaîne, ou même d’un dictionnaire ou d’un ensemble. Voici quelques exemple récapitulatifs, où on itère la fonction print sur des objets successifs (intervalle, liste, chaîne, tuple, ensemble, dictionnaire) :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
>>> obj = range(1,10) >>> for x in obj: print(x,end=' ') 1 2 3 4 5 6 7 8 9 >>> obj = [x*x for x in range(1,10)] >>> for x in obj: print(x,end=' ') 1 4 9 16 25 36 49 64 81 >>> obj = 'bien le bonjour' >>> for x in obj: print(x,end=' ') b i e n l e b o n j o u r >>> obj = tuple(range(1,10)) >>> for x in obj: print(x,end=' ') 1 2 3 4 5 6 7 8 9 >>> obj = set(range(10,40,3)) >>> obj {34, 37, 10, 13, 16, 19, 22, 25, 28, 31} >>> for x in obj: print(x,end=' ') 34 37 10 13 16 19 22 25 28 31 >>> obj = {x:x*x for x in range(1,10)} >>> for x in obj.keys(): print(x,end=' ') 1 2 3 4 5 6 7 8 9 >>> for x in obj.values(): print(x,end=' ') 1 4 9 16 25 36 49 64 81 >>> for (x,y) in obj.items(): print(x,y,end=' ') 1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 |
Les itérables sont donc les objets qu’on peut parcourir, soit parce qu’ils sont présents physiquement en mémoire, soit parce qu’ils sont capables de produire toutes leurs valeurs à la demande. Pour illustrer cette différence un peu subtile, on va construire un itérateur un peu particulier avec la fonction map :
|
1 2 3 4 5 6 7 8 9 10 |
>>> from sys import getsizeof # importe la fonction calculant la taille mémoire d'un objet >>> m = map(x:x*x, range(1,100)) # pour mapper la fonction x → x2 sur l'intervalle [1,99[ >>> getsizeof(m) # la taille occupée en mémoire est minuscule 64 >>> m # en fait m pointe sur un "map object" quelque part en mémoire <map object at 0x103d6bc90> >>> next(m), next(m), next(m) # la fonction next permet d'avancer dans l'itérateur (1,4,9) >>> next(m), next(m), next(m) # après les trois premières valeurs, les trois suivantes (16, 25, 36) |
On voit ici que notre objet map contient un mécanisme permettant de délivrer des valeurs successives (et qu’il est capable de mémoriser à quel stade il en est resté). On peut ainsi accéder manuellement aux valeurs successives de l’objet m, jusqu’à au dernier (un appel supplémentaire de next provoque alors l’erreur StopIteration) :
|
1 2 3 4 5 6 7 8 9 10 |
>>> while True: print(next(m),end=' ') 49 64 81 100 121 144 169 196 225 256 289 324 361 400 441 484 529 576 625 676 729 784 841 900 961 1024 1089 1156 1225 1296 1369 1444 1521 1600 1681 1764 1849 1936 2025 2116 2209 2304 2401 2500 2601 2704 2809 2916 3025 3136 3249 3364 3481 3600 3721 3844 3969 4096 4225 4356 4489 4624 4761 4900 5041 5184 5329 5476 5625 5776 5929 6084 6241 6400 6561 6724 6889 7056 7225 7396 7569 7744 7921 8100 8281 8464 8649 8836 9025 9216 9409 9604 9801 Traceback (most recent call last): File "<pyshell#57>", line 1, in <module> while True: print(next(m),end=' ') StopIteration |
Revenons maintenant à la définition initiale de l’objet m, et créons une liste obtenue en convertissant cet objet (avec le constructeur list). De cette manière, nous formons une liste m2 contenant simultanément toutes les valeurs qu’était capable de produire l’objet m. Deux remarques s’imposent : d’une part la taille de l’objet m2 est beaucoup plus grande que celle de m, et d’autre part la conversion de m en la liste m2 a visiblement “épuisé” le mécanisme d’itération de m :
|
1 2 3 4 5 6 7 8 |
>>> m = map(x:x*x, range(1,100)) # pour mapper la fonction x → x2 sur l'intervalle [1,99[ >>> m2 = list(m) # on convertit m en liste >>> getsizeof(m2) # la taille de m2 est beaucoup plus importante 992 >>> m2 # on a réduit ici l'affichage, qui tient sur plusieurs lignes [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, <...>, 9025, 9216, 9409, 9604, 9801] >>> list(m) # on voit que la conversion en liste a épuisé l'itérateur m ! [] |
Prenons maintenant un exemple analogue au précédent, mais obtenu avec la fonction filter
|
1 2 3 4 5 6 7 8 9 10 |
>>> f = filter(lambda x: x%5 == 3, range(1,1000)) # filtre dans [1,999] les congrus à 3 modulo 5 >>> f # f est un "filter objet" <filter object at 0x103d37a50> >>> getsizeof(f) # il occupe très peu de place en mémoire 64 >>> next(f), next(f), next(f) # on itère trois fois sur cet objet (3, 8, 13) >>> g = [x for x in range(1,1000) if x % 5 == 3 ] # liste des congrus à 3 modulo 5 dans [0,999] >>> getsizeof(g) # la taille est beaucoup plus importante 1680 |
Autre exemple, avec la fonction zip, qui forme des tuples d’éléments de même position dans différents itérables :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> z = zip([1,2,3],[4,5,6],[7,8,9]) # on va synchroniser trois listes de même longueur >>> z # le résultat est un "zip object" <zip object at 0x103d806c8> >>> next(z) # la première itération renvoie le tuple (1,4,7) (1, 4, 7) >>> next(z) # 2ème itération: le tuple des seconds (2, 5, 8) >>> next(z) # 3ème itération: le tuple des troisièmes (3, 6, 9) >>> next(z) # une itération de trop car c'était fini Traceback (most recent call last): File "<pyshell#100>", line 1, in <module> next(z) StopIteration |
Nous terminons ce tour d’horizon en évocant les “intervalles” (obtenus par la fonction range de Python), et qui ont été souvent utilisés jusqu’ici (notamment dans les boucles for) :
|
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> r = range(0,10000,25) # l'intervalle des valeurs de 0 à 9999, avec un pas de 5 >>> getsizeof(r) # ça ne prend pas de place en mémoire 48 >>> getsizeof(list(r)) # en revanche, si on forme la liste des valeurs.... 3720 >>> next(r) # tiens, les 'range' ne sont donc pas des itérateurs <...> TypeError: 'range' object is not an iterator >>> ir = iter(r) # en fait, le voici, l'itérateur associé au range r >>> ir <range_iterator object at 0x103d24e10> >>> next(ir), next(ir), next(ir) # trois itérations 'à la main' (0, 25, 50) |
Quand on évalue une boucle for pour parcourir un objet itérable, c’est le mécanisme next qui est mis en oeuvre dans les coulisses, l’exception StopIteration étant traitée par la boucle for elle-même.
Mais il en est ainsi de nombreux mécanismes qui opèrent sur des objets itérables (la fonction in pour tester l’appartenance, la fonction sum pour calculer une somme, etc).
Il y a une différence subtile entre “itérable” et “itérateur”, les itérateurs étant des fonctions permettant de parcourir les itérables (mais ça fait un peu jargon, tout ça) ; si cette distinction est pertinente pour les “range”, elle ne l’est pas pour les “zip”, “map” et autres “filter”, qui sont leur propre itérateur (got it ?)
Tout ça est en fait assez transparent dans la pratique. On retiendra surtout que l’utilisation d’itérateurs est un moyen efficace (l’empreinte mémoire est faible) d’accéder successivement aux différentes composantes d’un objet itérable (chaîne, liste, tuple, dictionnaire, ensemble, etc.) en évitant de créer simultanément toutes ces composantes en mémoire.
Dernière distinction intéressante : on peut créer plusieurs itérateurs sur un intervalle, ce qui permet de gérer simultanément plusieurs parcours de celui-ci.