Itérable
Un itérable est un objet capable d’énumérer ses éléments. Par définition, si on peut écrire :
for e in x:
...
alors x
est un itérable.
Les séquences (tuple, liste, string, etc.) et les dictionnaires sont des itérables.
1. Indiçable
Par définition, x
est indiçable (subscriptable) si l’expression x[i]
a un sens.
Pour un objet x
, récupérer la valeur de x[i]
, c’est récupérer ce que renvoie x.__getitem__(i)
.
Donc un indiçable est un objet possédant une méthode __getitem__(self,i)
.
La méthode n’impose aucune contrainte en ce qui concerne le type et les valeurs de son paramètre i
.
De même, modifier la valeur de x[i]
se fait via la méthode __setitem__()
, car :
x[i] = 1
est interprété comme :
x.__setitem__(i,1)
Itérer sur un indiçable
Si x
est indiçable, alors :
for e in x:
...
est interprété comme :
i=0
try:
while True:
e = x[i]
...
i += 1
except IndexError:
pass
Le for
n’est donc rien d’autre qu’un while
infini duquel on sort par une exception.
Mais pour que cela fonctionne sur un indiçable, il faut que celui-ci :
-
accepte au minimum des indices entiers positifs ou nuls
-
lance une exception
IndexError
pour arrêter l’itération
Exemple d’indiçable :
class Indicable():
def __getitem__(self,i):
if i>10: raise IndexError("index hors des limites")
return i*i
ce qui peut s’utiliser comme :
mon_indicable = Indicable()
for k in mon_indicable:
print(k,end=" ")
pour donner :
0 1 4 9 16 25 36 49 64 81 100
L’exception n’est pas perceptible par le développeur.
En effet, elle est attrapée et gérée par la boucle for
.
Par conséquent, elle s’évapore après traitement, et l’exécution du programme se poursuit normalement.
2. Itérable séquentiel
Il n’est pas obligatoire d’être indiçable pour être itérable.
Par exemple, le type set
est itérable, mais pas indiçable :
s = { 1,3,5,7,9, 1,3,5,7,9 }
for i in s:
print(i,end=" ") # 1 3 5 7 9
print(s[1]) # TypeError: 'set' object is not subscriptable
On accède séquentiellement aux éléments de l’itérable, sans pouvoir accéder directement à l’un d’eux en particulier.
Pour un objet non indiçable :
for e in x:
...
est interprété comme :
for e in iter(x):
...
ce qui donne, en décomposant le for
:
it = iter(x) (1)
try:
e = next(it) (2)
while True:
...
e = next(it) (2)
except StopIteration:
pass
1 | iter(x)] crée un nouvel objet… |
2 | … qui débite des valeurs lorsqu’on le sollicite avec la fonction next() . |
Comme précédement, le for
n’est rien d’autre qu’un while
infini duquel on sort par une exception.
L’objet créé par la fonction iter()
s’appelle un itérateur.
On constate que cet itérateur est ensuite manipulé par la fonction next()
.
3. Itérateur
Un itérateur égrène donc des valeurs :
un_iterateur = iter("abc") # itérateur sur une string
print(next(un_iterateur)) # -> a
print(next(un_iterateur)) # -> b
print(next(un_iterateur)) # -> c
print(next(un_iterateur)) # -> StopIteration
ou bien :
un_iterateur = iter({True,3,(1,2)}) # itérateur sur un set
print(next(un_iterateur)) # -> True
print(next(un_iterateur)) # -> (1,2)
print(next(un_iterateur)) # -> 3
print(next(un_iterateur)) # -> StopIteration
3.1. Itérateur créé sur une collection
Un itérateur est souvent utilisé dans une itération pour accéder séquentiellement aux éléments d’une collection sous-jacente. Il est associé dès sa création à cette collection, qu’il va parcourir séquentiellement et une seule fois.
Par analogie, on peut voir la collection comme une règle graduée, et l’itérateur comme un curseur posé sur la règle, se déplaçant de gauche à droite d’une position à la fois, et donnant accès à la cote sur laquelle il est positionné.

L’itérateur détient l’élément courant de l’itération. Donc un itérateur est relatif à une itération donnée sur une collection donnée. On peut exploiter plusieurs itérateurs sur un même itérable en même temps. Par exemple, lors de boucles imbriquées sur une même collection, on a autant d’itérateurs que de boucles.
Par exemple, le code ci-dessous qui sort les mots de 2 lettres prises parmi a,b,c,d :
s = "abcd"
for c1 in s:
for c2 in s:
print(f"{c1}{c2}",end=" ")
n’est qu’une écriture compacte pour :
s = "abcd"
for c1 in iter(s):
for c2 in iter(s):
print(f"{c1}{c2}",end=" ")
qui révèle la présence de 2 itérateurs.
La décomposition du for
donne :
s = "abcd"
it1 = iter(s) (1)
try:
while True:
c1 = next(it1)
it2 = iter(s) (2)
try:
while True:
c2 = next(it2)
print(f"{c1}{c2}",end=" ")
except StopIteration:
pass
except StopIteration:
pass
1 | création d’un premier itérateur it1 |
2 | création d’un second itérateur it2 |
3.2. iter()
et next()
Les fonctions iter()
et next()
appellent respectivement
les méthodes __iter__()
et __next__()
de l’objet passé en argument,
ce qui a le mérite d’allèger l’écriture qui sans cela serait :
s = "abcd"
# boucle de niveau 1
it1 = s.__iter__()
try:
while True:
c1 = it1.__next__()
# boucle de niveau 2
it2 = s.__iter__()
try:
while True:
c2 = it2.__next__()
print(f"{c1}{c2}",end=" ")
except StopIteration:
pass
# fin niveau 2
except StopIteration:
pass
# fin niveau 1
3.3. Implémenter un itérateur
Créer un itérateur revient à créer une classe possédant une méthode __next__()
renvoyant l’élément courant (celui sur lequel l’itérateur est positionné),
puis se positionnant sur le suivant (parcours séquentiel).
Pour signaler la fin de l’itération, l’itérateur doit lancer une exception de type StopIteration
.
3.4. Fonctions renvoyant un itérateur
range()
est une fonction de base (builtin function) :
for i in range(5):
print(i,end=" ") # 0 1 2 3 4
print(list(range(10))) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(set(range(10))) # {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
qui renvoye un itérateur :
import sys
print(sys.getsizeof(range(10))) # 48
print(sys.getsizeof(range(10000))) # 48
print(sys.getsizeof(list(range(10)))) # 136
print(sys.getsizeof(list(range(10000)))) # 80056
Il existe d’autres builtins fonctions renvoyant un itérateur, comme par exemple :
enumerate()
-
renvoie un itérateur qui égrène des tuples contenant un compteur de l’itération courante et l’élément courant de l’itérable passé en argument.
zip()
-
renvoie un itérateur qui égrène des tuples regroupant les éléments de chacun des itérables passés en argument.
filter()
-
renvoie un itérateur qui égrène les éléments de l’itérable passé en second argument vérifiant la condition exprimée par la fonction passée en premier argument.
map()
-
renvoie un itérateur qui égrène les valeurs calculées par la fonction passée en premier argument à partir des éléments de l’itérable passé en second argument.
4. Packing et unpacking
Le packing/unpacking est l’empaquetage/dépaquetage d’un itérable. Il permet, par exemple, en une instruction, d’affecter plusieurs variables en piochant dans un itérable :
s = "abcd"
s1,s2,s3,s4 = s
print(f"{s1=},{s2=},{s3=},{s4=}") # s1='a', s2='b', s3="c", s4="d"
Il suffit juste que le nombre de variables à gauche soit adéquat, sinon :
s1,s2,s3,s4,s5 = s # ValueError: not enough values to unpack (expected 5, got 4)
s1,s2,s3 = s # ValueError: too many values to unpack (expected 3)
Cela fonctionne à plusieurs niveaux d’imbrication :
l1,l2,l3 = [1,("a","b","c"),None]
print(f"{l1=},{l2=},{l3=}") # l1=1, l2=('a', 'b', 'c'), l3=None
l1,(l21,l22,l23),l3 = [1,("a","b","c"),None]
print(f"{l1=},{l21=},{l22=},{l23=},{l3=}") # l1=1, l21='a', l22='b', l23='c', l3=None
L’opérateur splat *
L’opérateur splat *
, utilisé devant une variable,
permet à celle-ci de capturer plusieurs éléments issus d’un dépaquetage :
s = "abcd"
s1,s2,*s3 = s # s1='a', s2='b', s3=['c','d']
s1,*s2 = s # s1='a', s2=['b','c','d']
s1,*s2,s3 = s # s1='a', s2=['b','c'], s3='d'
l1,(l21,*l22),*l3 = [1,("a","b","c"),None,"a"]
print(f"{l1=},{l21=},{l22=},{l3=}") # l1=1, l21='a', l22=['b','c'], l3=[None,"a"]
*l1,l2 = [1,("a","b","c"),None,"a"]
print(f"{l1=},{l2=}") # l1=[1, ('a', 'b', 'c'), None], l2='a'
Utilisé devant un itérable, il le dépaquette sur place :
print(*[0, 1, 2], 3, *[4, 5, 6]) # 0 1 2 3 4 5 6
On peut utiliser cela pour définir une fonction acceptant un nombre variable d’arguments :
def f(*args):
print(f"{len(args)=}, {args=}")
f(1,2,3) # len(args)=3, args=(1, 2, 3)
f("abc", None) # len(args)=2, args=('abc', None)
C’est équivalent à :
def f(args):
print(f"{len(args)=}, {args=}")
sauf que dans ce dernier cas, f
ne peut recevoir qu’un unique argument,
ce qui oblige, si on veut lui en fournir plusieurs, de les empaqueter dans un tuple (ou une liste, ou un ensemble) :
f((1,2,3)) # len(args)=3, args=(1, 2, 3)
f(("abc", None)) # len(args)=2, args=('abc', None)
C’est moins élégant pour l’utilisateur de la fonction.