Retour sur les boucles

Cas des boucles infinies

Nous avons vu les boucles for pour itérer sur des données, puis les boucles while pour boucler sur une condition. Et nous avons vu que, volontairement ou non, nous pouvions tomber dans des cas de boucles infinies.

while True:
    print("Vers l'infini et au-delà !")

Pour rappel, utilisez la combinaison de touches Ctrl+C pour couper le programme.

Volontairement, ça peut être pour laisser tourner un programme en tâche de fond — un serveur par exemple — qui s’exécuterait continuellement pour traiter des requêtes. Et dans ce cas des dispositifs seront mis en place pour terminer proprement le programme quand on le souhaite.

Mais il y a d’autres cas d’usages légitimes de boucles a priori infinies, car il existe d’autres moyens de terminer une boucle en cours d’exécution.

En effet, la condition d’un while est parfois difficile à exprimer, d’autant plus si elle repose sur des événements tels que des input. Dans ce cas, un idiome courant est d’écrire une boucle infinie et d’utiliser un autre moyen de sortir de la boucle : le mot-clé break.
Ce mot-clé, quand il est rencontré, a pour effet de stopper immédiatement la boucle en cours, sans repasser par la condition.

while True:
    value = input('Entrez un nombre: ')
    if value.isdigit():
        value = int(value)
        break
    else:
        print('Nombre invalide')

Avec cette boucle, nous attendons que l’entrée ne soit composée que de chiffres, auquel cas on rentre dans le if et l’on atteint le break. Sinon, on continue de boucler en redemandant à l’utilisateur de saisir un nouveau nombre.

La boucle, infinie en apparence (while True), possède en fait une condition de fin exprimée par un if.

Contrôle du flux

break permet donc de stopper la boucle. Il n’est pas seulement disponible pour les boucles while, on peut aussi l’utiliser dans un for.

>>> for i in range(10):
...     print(i)
...     if i == 5:
...         break
... 
0
1
2
3
4
5

Comme précédemment, la sortie de boucle est immédiate, l’effet ne serait donc pas le même si le print était placé après le bloc if.

>>> for i in range(10):
...     if i == 5:
...         break
...     print(i)
... 
0
1
2
3
4

Il faut savoir que dans le cas de boucles imbriquées, break ne se rapporte qu’à la boucle juste au-dessus. Il n’est pas possible d’influer sur les boucles extérieures.

>>> for x in range(3):
...     for y in range(3):
...         if y == 2:
...             break
...         print(x, y)
... 
0 0
0 1
1 0
1 1
2 0
2 1

Mais break n’est pas le seul mot-clé de contrôle du flux d’une boucle et je vais maintenant vous parler de continue.

continue permet aussi de terminer immédiatement l’itération en cours, mais pour passer à la suivante. Quand un continue est rencontré, on est directement conduit à la ligne d’introduction de la boucle et sa condition est réévaluée.

while True:
    value = input('Entrez un nombre: ')
    if not value:
        break
    if not value.isdigit():
        print('Nombre invalide')
        continue
    value = int(value)
    print(f'{value} * 2 = {value * 2}')

C’est un mot-clé très utile quand on traite une liste de données et que l’une des valeurs est invalide, on peut alors simplement l’ignorer et passer à la suivante.

values = [1, 2, 3, -1, 4, 5]

total = 0
for value in values:
    if value < 0:
        print('Invalid value', value)
        continue
    total += value

On a aussi le mot-clé else qui est assez facile à comprendre sur une boucle while : il intervient après la boucle si la condition a été évaluée comme fausse.

pv = 50

while pv > 0:
    print(f'Pythachu a {pv} PV')
    pv -= 20
    print('Pythachu perd 20 PV')
else:
    print('Pythachu est KO')

Le else intervient donc dans tous les cas… sauf si on a quitté la boucle sans réévaluer la condition (qui ne peut donc pas être fausse), c’est-à-dire en utilisant un break.
Ainsi, else permet de savoir comment s’est terminée une boucle, si on en est sorti normalement (auquel cas on passe dans le bloc) ou si on l’a interrompue (le bloc n’est pas exécuté).

pv = 50

while pv > 0:
    print(f'Pythachu a {pv} PV')
    degats = input('Nombre de degats : ')
    if not degats.isdigit():
        break
    degats = int(degats)
    pv -= degats
    print(f'Pythachu perd {degats} PV')
else:
    print('Pythachu est KO')

else est aussi applicable à la boucle for en ayant le même effet, il permet de savoir si la boucle est arrivée jusqu’au bout sans être interrompue.

Ainsi, sans break le else est bien exécuté.

>>> for i in range(5):
...     print(i)
... else:
...     print('end')
... 
0
1
2
3
4
end

Avec un break il ne l’est pas.

>>> for i in range(5):
...     print(i)
...     if i == 3:
...         break
... else:
...     print('end')
... 
0
1
2
3

Le mot-clé else est souvent mal compris — on pourrait croire qu’on entre dans le else uniquement s’il n’y a pas eu d’itérations — et donc peu recommandé pour lever toute ambiguïté.

Outils

Le monde de l’itération est très vaste en Python, les itérables se retrouvent au cœur de nombreux mécanismes. C’est pourquoi Python propose de base de nombreux outils relatifs à l’itération tels que les fonctions all et any que l’on a déjà vues.

Vous êtes-vous déjà demandé comment itérer simultanément sur plusieurs listes ou comment répéter une liste ? Ce chapitre est fait pour vous !

Fonctions natives (builtins)

On a déjà vu un certain nombre de builtins dans les chapitres précédents, mais il en reste quelques unes très intéressantes que j’ai omises jusqu’ici.

enumerate

Notamment la fonction enumerate, qui prend une liste (ou n’importe quel itérable) et permet d’itérer sur ses valeurs tout en leur associant leur index. C’est-à-dire que pour chaque valeur on connaîtra la position qu’elle occupe dans la liste.

>>> values = ['abc', 'def', 'ghi']
>>> for i, value in enumerate(values):
...     print(i, ':', value)
... 
0 : abc
1 : def
2 : ghi

Cela remplace aisément les constructions à base de range(len(values)) que l’on voit trop souvent et qui sont à éviter.

>>> for i in range(len(values)):
...     print(i, ':', values[i])
... 
0 : abc
1 : def
2 : ghi

On les évite justement parce qu'enumerate répond mieux au problème tout en étant plus polyvalent (on peut par exemple itérer sur un fichier), et qu’on a directement accès à la valeur (value) sans besoin d’une indirection supplémentaire par le conteneur (values[i]).

On notera au passage que la fonction enumerate accepte un deuxième argument pour préciser l’index de départ, qui est par défaut de zéro.

>>> with open('corbeau.txt') as f:
...     for i, line in enumerate(f, 1):
...         print(i, ':', line.rstrip())
... 
1 : Maître Corbeau, sur un arbre perché,
2 : Tenait en son bec un fromage.
3 : Maître Renard, par l'odeur alléché,
4 : Lui tint à peu près ce langage :
5 : Et bonjour, Monsieur du Corbeau.
6 : Que vous êtes joli ! que vous me semblez beau !
7 : Sans mentir, si votre ramage
8 : Se rapporte à votre plumage,
9 : Vous êtes le Phénix des hôtes de ces bois.
10 : À ces mots, le Corbeau ne se sent pas de joie ;
11 : Et pour montrer sa belle voix,
12 : Il ouvre un large bec, laisse tomber sa proie.
13 : Le Renard s'en saisit, et dit : Mon bon Monsieur,
14 : Apprenez que tout flatteur
15 : Vit aux dépens de celui qui l'écoute.
16 : Cette leçon vaut bien un fromage, sans doute.
17 : Le Corbeau honteux et confus
18 : Jura, mais un peu tard, qu'on ne l'y prendrait plus.
reversed

reversed est une fonction très simple, elle permet d’inverser une séquence d’éléments, pour les parcourir dans l’ordre inverse.

>>> values = ['abc', 'def', 'ghi']
>>> for value in reversed(values):
...     print(value)
... 
ghi
def
abc

La fonction ne modifie pas la séquence initiale (contrairement à la méthode reverse des listes).

>>> values
['abc', 'def', 'ghi']
sorted

Dans la même veine on a la fonction sorted, semblable à la méthode sort des listes mais renvoyant ici une copie.

>>> values = [5, 3, 2, 4, 6, 1, 9, 7, 8]
>>> sorted(values)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> values
[5, 3, 2, 4, 6, 1, 9, 7, 8]

On notera que le tri se fait en ordre croissant (les plus petits éléments d’abord) par défaut, mais la fonction accepte un argument reverse pour trier en ordre décroissant (les plus grands d’abord).

>>> sorted(values, reverse=True)
[9, 8, 7, 6, 5, 4, 3, 2, 1]

Mieux encore, la fonction propose un paramètre key pour personnaliser la manière dont seront triés nos éléments. C’est une fonction qui recevra un élément en paramètre et renverra une valeur (par exemple un nombre), le tri se fera alors suivant l’ordre entre ces valeurs renvoyées.

Les fonctions en Python sont des valeurs comme les autres que l’on peut donc parfaitement passer en argument. Ces arguments-fonctions sont généralement appelés des callbacks (ou « fonctions de rappel »).

Par exemple, le tri par défaut pour les chaînes de caractères est l’ordre lexicographique (plus ou moins équivalent à l’ordre alphabétique).

>>> words = ['zèbre', 'autruche', 'cheval', 'oie']
>>> sorted(words)
['autruche', 'cheval', 'oie', 'zèbre']

On pourrait alors préciser une fonction de tri key=len pour les trier par taille.

>>> sorted(words, key=len)
['oie', 'zèbre', 'cheval', 'autruche']

En effet, la fonction len sera appelée pour chaque mot et les mots seront triés suivant le retour de la fonction (en l’occurrence 3, 5, 6 et 8). Mais il est possible d’utiliser n’importe quelle fonction en tant que clé de tri, tant que cette fonction renvoie quelque chose d’ordonnable.

Voici un autre exemple avec une fonction pour trier les mots dans l’ordre alphabétique mais en commençant par la dernière lettre du mot.

>>> def key_func(word):
...     return word[::-1] # On renvoie le mot à l'envers
...
>>> key_func('autruche')
'ehcurtua'
>>> sorted(words, key=key_func)
['autruche', 'oie', 'zèbre', 'cheval']

Ces deux arguments sont aussi disponibles sur la méthode sort des listes.

>>> words.sort(key=len, reverse=True)
>>> words
['autruche', 'cheval', 'zèbre', 'oie']
min et max

On a déjà vu les fonctions min et max qui permettent respectivement de récupérer le minimum/maximum parmi leurs arguments.

>>> min(3, 1, 2)
1
>>> max(3, 1, 2)
3

On sait aussi qu’on peut les appeler avec un seul argument (un itérable) et récupérer le minimum/maximum dans cet itérable.

>>> min({3, 1, 2})
1
>>> max([3, 1, 2])
3

Mais sachez maintenant que ces fonctions acceptent aussi un argument key qui fonctionne de la même manière que pour sorted.
Ainsi il est possible d’expliquer comment doivent être comparées les valeurs. On peut alors simplement demander la valeur minimale/minimale d’une liste en comparant les nombres selon leur valeur absolue.

>>> min([-5, -2, 1, 3], key=abs)
1
>>> max([-5, -2, 1, 3], key=abs)
-5

Ces fonctions acceptent aussi un argument default dont la valeur est renvoyée (plutôt qu’une erreur) si l’itérable est vide.

>>> min([])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: min() arg is an empty sequence
>>> min([], default=42)
42
zip

zip est une fonction très pratique de Python, puisqu’elle permet de parcourir simultanément plusieurs itérables. On appelle la fonction en lui fournissant nos itérables en arguments, et l’on itère ensuite sur l’objet qu’elle nous renvoie.
Les éléments que l’on obtient alors sont des tuples formés des éléments de nos itérables de départ.

>>> for elem in zip(words, 'abcd', range(4)):
...     print(elem)
... 
('autruche', 'a', 0)
('cheval', 'b', 1)
('zèbre', 'c', 2)
('oie', 'd', 3)

Il est ainsi possible d’utiliser l'unpacking de Python pour avoir quelque chose de plus explicite.

>>> for word, letter, number in zip(words, 'abcd', range(4)):
...     print(word, letter, number)
... 
autruche a 0
cheval b 1
zèbre c 2
oie d 3

zip accepte autant d’arguments que l’on souhaite, on peut l’appeler avec deux itérables comme avec dix.

Aussi, il s’arrête dès que l’un des itérables se termine, puisqu’il ne peut alors plus produire de tuple contenant un élément de chaque.

>>> for i, j in zip(range(2, 6), range(10)):
...     print(i, j)
... 
2 0
3 1
4 2
5 3
Module itertools

En plus des outils built-in pour manipuler les itérables, la bibliothèque standard fournit aussi une mine d’or : le module itertools.

Je ne détaillerai pas tout ce que contient le module, la documentation fera cela beaucoup mieux que moi. Je veux juste vous présenter quelques fonctions qui pourraient vous être bien utiles.

chain

Comme son nom l’indique, chain permet de chaîner plusieurs itérables, de façon transparente et quels que soient leurs types.

>>> from itertools import chain
>>> for letter in chain('ABC', ['D', 'E'], ('F', 'G')):
...     print(letter)
... 
A
B
C
D
E
F
G
zip_longest

zip_longest est un équivalent à zip qui ne s’arrête pas au premier itérable terminé mais qui continue jusqu’au dernier. Les valeurs manquantes seront alors complétées par None, ou par la valeur précisée au paramètre fillvalue.

>>> from itertools import zip_longest
>>> for i, j in zip_longest(range(2, 6), range(10)):
...     print(i, j)
... 
2 0
3 1
4 2
5 3
None 4
None 5
None 6
None 7
None 8
None 9
>>> for letter1, letter2 in zip_longest('ABCD', 'EF', fillvalue='.'):
...     print(letter1, letter2)
... 
A E
B F
C .
D .
product

product calcule le produit cartésien entre plusieurs itérables, c’est-à-dire qu’il produit toutes les combinaisons d’éléments possibles.

>>> from itertools import product
>>> for i, c in product(range(5), 'ABC'):
...     print(i, c)
... 
0 A
0 B
0 C
1 A
1 B
1 C
2 A
2 B
2 C
3 A
3 B
3 C
4 A
4 B
4 C

Cela revient à écrire des boucles for imbriquées tout en économisant des niveaux d’indentation. L’exemple précédent est ainsi équivalent au code suivant.

for i in range(5):
    for c in 'ABC':
        print(i, c)

Le module propose d’autres fonctions combinatoires que je vous invite à regarder.

Recettes

En plus de donner des explications et exemples pour chacune de ses fonctions, la documentation du module itertools fournit aussi quelques « recettes ».

Il s’agit de fonctions qui répondent à des besoins trop particuliers pour être vraiment intégrées au module. Les recettes sont là pour que vous les repreniez dans votre code et que vous les adaptiez à votre convenance.

Listes en intension

On a vu qu’il était possible d’écrire des conditions sous forme d’expressions, qu’en est-il des boucles ?

Une expression est une instruction qui possède une valeur. Pour une condition c’est facile : on a une valeur si la condition est vraie et une autre valeur sinon. Mais quelle pourrait être la valeur d’une boucle ?

Il n’y a pas de réponse évidente à cette question, et c’est pourquoi il n’y a pas d’expression générale pour exécuter une boucle. Il existe en revanche les listes en intension, qui permettent de construire une liste à partir d’une boucle for.

L’intension est un concept mathématique qui s’oppose à l’extension pour définir un ensemble1. La définition par extension, c’est celle que nous avons utilisée jusqu’ici, qui consiste à définir l’ensemble par les éléments qu’il possède.

powers = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]

La définition par intension consiste elle à décrire l’ensemble selon une règle, par exemple « les dix premières puissances de 2 ». On la traduirait en Python par le code suivant :

>>> powers = [2**i for i in range(10)]
>>> powers
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]

On voit alors que l’on utilise le for dans une expression pour construire une liste. Le code précédent est équivalent à la boucle suivante.

powers = []
for i in range(10):
    powers.append(2**i)

On peut ainsi transposer vers une liste en intension toute boucle for ne consistant qu’à évaluer une expression à chaque itération.

>>> [letter + '!' for letter in 'ABCD']
['A!', 'B!', 'C!', 'D!']
>>> [len(word) for word in ['zeste', 'de', 'savoir']]
[5, 2, 6]
>>> [letter * i for letter, i in zip('ABCD', range(1, 5))]
['A', 'BB', 'CCC', 'DDDD']

Le terme anglais pour les listes en intension est list comprehensions, aussi il est courant de rencontrer en français les expressions « liste en compréhension » ou « compréhension de liste », il s’agit évidemment de la même chose.

Conditions de filtrage

Mais les listes en intension ne s’arrêtent pas là et permettent des constructions plus complexes : il est possible de filtrer les éléments à intégrer ou non à la liste. Pour cela on utilise une expression de la forme suivante.

[expression for item in iterable if condition]

La condition interviendra à chaque itération et déterminera s’il faut ajouter expression aux éléments de la liste en construction ou non. Voici par exemple la liste des entiers naturels pairs strictement inférieurs à 10.

>>> [i for i in range(10) if i % 2 == 0]
[0, 2, 4, 6, 8]

Ce code est équivalent à la boucle suivante :

values = []
for i in range(10):
    if i % 2 == 0:
        values.append(i)

Attention à ne pas confondre le if utilisé ici avec le if de l’expression conditionnelle. Ce premier n’autorise pas le else puisque cela n’aurait pas de sens sur une condition de filtrage.

Par ailleurs, les expressions conditionnelles étant des expressions à part entière, il est parfaitement possible de les utiliser dans des listes en intension.

>>> [i // 2 if i % 2 == 0 else i * 3 + 1 for i in range(10)]
[0, 4, 1, 10, 2, 16, 3, 22, 4, 28]

On peut même les combiner aux conditions de filtrage sans que cela ne pose problème, veillez tout de même à ce que le code reste toujours lisible.

>>> [i // 2 if i % 2 == 0 else i * 3 + 1 for i in range(10) if i % 3 == 0]
[0, 10, 3, 28]

Pour plus de clarté, il est ainsi parfois conseillé de placer des parenthèses autour de l’expression conditionnelle. Mais de manière générale, une liste en intension trop longue peut signifier que ce n’est pas la meilleure solution au problème et qu’une boucle « standard » irait tout aussi bien.

>>> [(i // 2 if i % 2 == 0 else i * 3 + 1) for i in range(10) if i % 3 == 0]
[0, 10, 3, 28]

Il est aussi possible d’utiliser plusieurs if dans l’intension pour définir plusieurs conditions sur lesquelles filtrer, celles-ci s’additionnant les unes aux autres.

>>> [i for i in range(10) if i % 2 == 0 if i % 3 == 0] # Multiples de 2 et 3
[0, 6]
Boucles imbriquées

D’ailleurs, les for aussi peuvent être chaînés au sein d’une même intension. Cela permet alors de faire la même chose qu’avec des boucles imbriquées pour remplir notre liste.

>>> [(i, c) for i in range(3) for c in 'AB']
[(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')]

Les boucles sont à lire de gauche à droite comme si elles étaient écrites de haut en bas, le code précédent est équivalent à :

values = []
for i in range(3):
    for c in 'AB':
        values.append((i, c))

Et il est possible d’enchaîner autant de for que l’on veut dans l’intension, comme l’on pourrait en imbriquer autant qu’on veut. Mais attention, nous obtenons bien une seule liste en sortie, comportant toutes les combinaisons parcourues lors de l’itération.

Les listes en intension étant des expressions comme les autres, il est aussi possible d’imbriquer les intensions. C’est ainsi que l’on peut construire des listes à plusieurs dimensions.

>>> table = [[0 for x in range(3)] for y in range(2)]
>>> table
[[0, 0, 0], [0, 0, 0]]

C’est un modèle de construction assez courant en Python puisqu’il ne souffre pas du problème de références multiples dont je parlais lors de la présentation des listes. Ici, chaque sous-liste est une instance différente et peut donc être modifiée indépendamment des autres.

>>> table[0][1] = 5
>>> table
[[0, 5, 0], [0, 0, 0]]

Souvenez-vous, ce n’est pas le résultat qu’on obtenait avec [[0] * 3] * 2 où chaque ligne était une référence vers la même liste.

>>> table = [[0] * 3] * 2
>>> table
[[0, 0, 0], [0, 0, 0]]
>>> table[0][1] = 5
>>> table
[[0, 5, 0], [0, 5, 0]]
Autres constructions en intension

On parle souvent de listes en intension mais ce n’est pas le seul type qui peut être construit ainsi. Au programme, on trouve aussi les ensembles et les dictionnaires.

Pour les ensembles, la syntaxe est identique aux listes à l’exception qu’on utilise des accolades plutôt que des crochets.

>>> {i**2 for i in range(10)}
{0, 1, 64, 4, 36, 9, 16, 49, 81, 25}

Et on retrouve les mêmes fonctionnalités sur les intensions : il est possible d’avoir plusieurs boucles et d’utiliser des conditions de filtrage.

>>> {i+j for i in range(10) for j in range(10) if (i+j) % 2 == 0}
{0, 2, 4, 6, 8, 10, 12, 14, 16, 18}

Vous constaterez pour ce dernier exemple que le résultat ne serait pas du tout le même avec une liste, l’ensemble ne permettant pas les duplications.

Pour les dictionnaires on retrouve quelque chose de similaire mais utilisant la syntaxe cle: valeur plutôt qu’une simple expression (où cle et valeur sont aussi des expressions).

>>> {i: i**2 for i in range(10)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

Itérateurs

Itérables et itérateurs

Depuis plusieurs chapitres j’utilise le terme d’itérables pour qualifier les objets qui peuvent être parcourus à l’aide d’une boucle for, mais qu’en est-il ? On a vu qu’il existait un grand nombre d’itérables, tels que les chaînes de caractères, les listes, les range, les dictionnaires, les fichiers, etc.

Il y en a d’autres encore et l’on en a vu plus récemment dans ce chapitre : les retours des fonctions enumerate ou zip sont aussi des itérables. Mais si on les regarde de plus près, on voit qu’ils sont un peu particuliers.

>>> enumerate('abcde')
<enumerate object at 0x7f30749e0240>
>>> zip('abc', 'def')
<zip object at 0x7f30749e02c0>

Ou plutôt on ne voit pas grand chose justement, ces objets sont assez intrigants. On sait qu’ils sont itérables, on l’a vu plus tôt, et on peut donc se servir de cette propriété pour les transformer en liste si c’est ce qui nous intéresse.

>>> list(enumerate('abcde'))
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e')]
>>> list(zip('abc', 'def'))
[('a', 'd'), ('b', 'e'), ('c', 'f')]

Mais ce qui est plus étonnant c’est qu’on ne peut itérer dessus qu’une seule fois.

>>> values = enumerate('abcde')
>>> for v in values:
...     print(v)
... 
(0, 'a')
(1, 'b')
(2, 'c')
(3, 'd')
(4, 'e')
>>> for v in values:
...     print(v)
...

On constate le même comportement avec la conversion en liste.

>>> values = zip('abc', 'def')
>>> list(values)
[('a', 'd'), ('b', 'e'), ('c', 'f')]
>>> list(values)
[]

Une fois parcourus une première fois, il n’est plus possible d’itérer à nouveau sur leurs valeurs. Contrairement à d’autres itérables comme les listes ou les ranges que l’on parcourt autant de fois que l’on veut.

En fait, ces objets enumerate et zip ne sont pas seulement des itérables, ils sont des itérateurs. Un itérateur peut se voir comme un curseur qui se déplace le long d’un itérable, et qui logiquement se consume à chaque étape. Ici l’objet enumerate est donc un itérateur le long de notre chaîne 'abcde'.

La fonction next en Python permet de récupérer la prochaine valeur d’un itérateur. Elle prend l’itérateur en argument et renvoie la valeur pointée par le curseur tout en le faisant avancer. Puisque l’itérateur avance, le retour de la fonction sera différent à chaque appel.

>>> values = enumerate('abcde')
>>> next(values)
(0, 'a')
>>> next(values)
(1, 'b')
>>> next(values)
(2, 'c')

En fin de parcours, l’itérateur lève une exception StopIteration pour signaler que l’itération est terminée.

>>> next(values)
(3, 'd')
>>> next(values)
(4, 'e')
>>> next(values)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

On ne peut alors pas revenir en arrière : une fois notre itérateur parcouru il est entièrement consumé. C’est pourquoi il n’est pas possible de faire deux for à la suite sur un même objet enumerate ou zip, ils sont à usage unique.

À noter que la fonction next accepte un second argument qui est la valeur à renvoyer dans le cas où l’itérateur est consumé plutôt que lever une exception.

>>> next(values, '')
''

Mais ces objets se basent sur des itérables réutilisables que sont les chaînes de caractères, listes ou autres : on peut donc à nouveau appeler enumerate pour obtenir un itérateur tout neuf et recommencer à boucler.

>>> values = 'abcde'
>>> for v in enumerate(values):
...     print(v)
... 
(0, 'a')
(1, 'b')
(2, 'c')
(3, 'd')
(4, 'e')
>>> for v in enumerate(values):
...     print(v)
... 
(0, 'a')
(1, 'b')
(2, 'c')
(3, 'd')
(4, 'e')
Fonctions map et filter

En évoquant les outils d’itération plus tôt, j’ai volontairement omis les fonctions map et filter. Parce que leurs fonctionnalités sont couvertes par les listes en intension et parce qu’elles renvoient des itérateurs.

map et filter sont issues de la programmation fonctionnelle et servent respectivement à convertir et à filtrer les données d’un itérable.

map prend en arguments une fonction et un itérable, et applique la fonction à chaque élément de l’itérable, renvoyant un itérateur sur les résultats.

>>> values = [1.3, 2.5, 3.8, 4.2]
>>> map(round, values)
<map object at 0x7f4ae2db16a0>
>>> list(map(round, values))
[1, 2, 4, 4]

Cela revient donc à utiliser la liste en intension suivante.

>>> [round(v) for v in values]
[1, 2, 4, 4]

filter est le pendant pour le filtrage des éléments. Ici le premier argument est une fonction utilisée comme prédicat : l’élément est conservé si le prédicat et vrai et ignoré sinon.

>>> def greater_than_two(n):
...     return n >= 2
... 
>>> list(filter(greater_than_two, values))
[2.5, 3.8, 4.2]

Ici, la liste en intension équivalente serait la suivante.

>>> [v for v in values if v >= 2]
[2.5, 3.8, 4.2]

map et filter existaient avant les listes en intension et sont moins utilisées aujourd’hui, surtout lorsqu’il s’agit de les transformer en listes. Elles restent parfois utilisées quand on n’attend rien de plus qu’un itérateur, par exemple pour fournir en argument à une autre fonction.

C’est le cas de str.join qui attend un itérable de chaînes de caractères et nécessite donc que les données soient converties en chaînes, ce que permet map.

>>> ', '.join(map(str, values))
'1.3, 2.5, 3.8, 4.2'
Itérateurs infinis

Comme je disais, un itérateur ne représente qu’un curseur, il a donc une empreinte très faible en mémoire. Mieux encore, il n’a même pas besoin de s’appuyer sur des données qui existent déjà, celles-ci peuvent être générées à la volée lors du parcours.

C’est déjà le principe des objets range qui occupent très peu d’espace : tous les nombres de l’intervalle ne sont pas stockés en mémoire à la création du range, ils sont simplement calculés pendant l’itération et disparaissent après.

On peut pousser le concept plus loin et itérer sur des données qui ne pourraient jamais tenir dans la mémoire de l’ordinateur, des données infinies. C’est le cas des itérateurs que nous allons voir ici, ils ne se terminent jamais.
Ces itérateurs infinis sont tirés du module itertools.

Le plus simple d’entre tous c’est count, qui permet de compter de 1 en 1.

>>> from itertools import count
>>> numbers = count()
>>> next(numbers)
0
>>> next(numbers)
1
>>> next(numbers)
2

À quoi cela peut-il servir ? C’est très pratique pour générer des identifiants uniques puisque chaque appel à next renverra un nombre différent.

>>> id_seq = count()
>>> def new_event():
...     return {'id': next(id_seq), 'type': 'monstre', 'message': 'Un pythachu sauvage apparaît'}
... 
>>> new_event()
{'id': 0, 'type': 'monstre', 'message': 'Un pythachu sauvage apparaît'}
>>> new_event()
{'id': 1, 'type': 'monstre', 'message': 'Un pythachu sauvage apparaît'}
>>> new_event()

Cela peut être aussi utile mathématiquement, pour simplement calculer un seuil à partir duquel une propriété est vraie.

>>> for i in count():
...     if 2**i > 1000:
...         break
... 
>>> i
10

On sait ainsi que 2102^{10} est la première puissance de 2 à être supérieur à 1000.

On notera que count peut prendre deux arguments : le premier est le nombre de départ (0 par défaut) et le second est le pas (1 par défaut).

>>> numbers = count(1, 2)
>>> next(numbers)
1
>>> next(numbers)
3
>>> next(numbers)
5

Un autre itérateur infini est repeat, qui répète simplement en boucle le même élément.

>>> from itertools import repeat
>>> values = repeat('hello')
>>> next(values)
'hello'
>>> next(values)
'hello'

On pourra le voir utilisé dans des zip pour simuler une séquence de même longueur qu’une autre.

>>> def additions(seq1, seq2):
...     for i, j in zip(seq1, seq2):
...         print(f'{i} + {j} = {i+j}')
... 
>>> additions(range(10), repeat(5))
0 + 5 = 5
1 + 5 = 6
2 + 5 = 7
3 + 5 = 8
4 + 5 = 9
5 + 5 = 10
6 + 5 = 11
7 + 5 = 12
8 + 5 = 13
9 + 5 = 14

repeat peut aussi prendre un argument qui indique le nombre de répétitions à effectuer, auquel cas il ne sera plus infini.

>>> list(repeat('hello', 5))
['hello', 'hello', 'hello', 'hello', 'hello']

Dans le même genre on trouve enfin cycle pour boucler (indéfiniment) sur un même itérable.

>>> from itertools import cycle
>>> values = cycle(['hello', 'world'])
>>> next(values)
'hello'
>>> next(values)
'world'
>>> next(values)
'hello'

C’est aussi un cas d’utilisation pour avoir un itérable que l’on voudrait au moins aussi grand qu’un autre.

>>> additions(range(10), cycle([3, 5, 8]))
0 + 3 = 3
1 + 5 = 6
2 + 8 = 10
3 + 3 = 6
4 + 5 = 9
5 + 8 = 13
6 + 3 = 9
7 + 5 = 12
8 + 8 = 16
9 + 3 = 12
Fonction iter

Pour terminer ce chapitre je voudrais vous parler d'iter, une fonction qui renvoie un simple itérateur sur l’itérable donné en argument. Un nouvel itérateur est construit et renvoyé à chaque appel sur l’itérable.

>>> values = [0, 1, 2, 3, 4]
>>> iter(values)
<list_iterator object at 0x7f3074a28850>
>>> iter(values)
<list_iterator object at 0x7f3074a28bb0>

Ces itérateurs sont semblables à nos objets enumerate, on peut appeler next dessus et récupérer la valeur suivante. Ils sont donc utiles si l’on souhaite parcourir manuellement un itérable à coups de next.

>>> it = iter(values)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2

Et bien sûr on peut aussi les parcourir avec un for. Attention encore, l’itérateur avance pendant le parcours, et le for continuera donc l’itération à partir d’où il se trouve.

>>> for v in it:
...     print(v)
... 
3
4
>>> for v in it:
...     print(v)
... 

Les itérateurs étant des itérables, il est possible de les donner à leur tour à iter. La fonction renverra alors simplement le même itérateur.

>>> it
<list_iterator object at 0x7f3074a21070>
>>> iter(it)
<list_iterator object at 0x7f3074a21070>

On constate bien que les deux valeurs ont la même adresse.