Objectifs du chapitre
À la fin de ce chapitre, vous serez capable de:
- écrire une boucle
whilecorrecte, c'est-à-dire munie de ses trois pièces obligatoires (initialisation, condition, progression), et reconnaître une boucle infinie avant de l'exécuter; - construire un compteur et un accumulateur, et choisir la valeur d'initialisation qui convient, y compris pour un maximum ou un minimum;
- énoncer l'invariant et le variant d'une boucle simple, et dresser la trace de ses variables tour par tour;
- utiliser
foravecrange(n),range(a, b)etrange(a, b, pas), et expliquer la convention de borne exclue ainsi que les erreurs de décalage qu'elle évite et celles qu'elle provoque; - décider, devant un problème, s'il appelle un
forou unwhile, et employerbreaketcontinuelà où ils clarifient le code; - écrire et exécuter une boucle imbriquée, et compter le nombre de tours qu'elle effectue.
Répéter sans se répéter
Un programme écrit sept fois
Voici le problème le plus banal qui soit: on relève la température moyenne de chaque jour d'une semaine, et l'on veut le total et la moyenne. Les sept valeurs, en degrés Celsius, sont celles du cours — des données fictives, qui reviendront au chapitre 8 lorsque nous les lirons dans un fichier: 2,4, 5,1, 7,8, 6,2, 3,9, −0,5 et 1,7.
Avec ce que vous savez des chapitres 1 et 2 — des variables, input(), float(), print() — vous savez déjà écrire ce programme. Il ressemble à ceci.
# Sept jours, sept fois la meme instruction
total = 0.0
total = total + float(input("Jour 1 - temperature: "))
total = total + float(input("Jour 2 - temperature: "))
total = total + float(input("Jour 3 - temperature: "))
total = total + float(input("Jour 4 - temperature: "))
Exécutons-le. Dans les blocs de résultat de ce chapitre, les valeurs que vous tapez apparaissent juste après l'invite, comme dans un vrai terminal: la ligne Jour 1 - temperature: 2.4 se compose de l'invite affichée par le programme et du 2.4 que vous avez frappé au clavier.
Jour 1 - temperature: 2.4
Jour 2 - temperature: 5.1
Jour 3 - temperature: 7.8
Jour 4 - temperature: 6.2
Jour 5 - temperature: 3.9
Jour 6 - temperature: -0.5
Jour 7 - temperature: 1.7
Total : 26.599999999999998
Moyenne : 3.8
Le programme fonctionne. La moyenne vaut 3,8 °C, ce qui est bien la valeur attendue. Au passage, vous retrouvez l'honnêteté des flottants du chapitre 1: la somme s'affiche 26.599999999999998 et non 26.6, parce que 2,4 et 5,1 ne sont pas représentables exactement en binaire. La division par 7 retombe, elle, sur 3.8; ne vous y fiez pas, c'est un heureux hasard de l'arrondi. Nous y reviendrons.
Mais regardez le code, pas le résultat. Sept lignes rigoureusement identiques à un chiffre près. Trois défauts, et ils sont graves.
Premier défaut: le code grandit avec les données. Pour un mois, il faudrait trente lignes; pour une année, trois cent soixante-cinq. Un programme dont la longueur est proportionnelle à la quantité de données n'est pas un programme, c'est une transcription.
Deuxième défaut: le nombre de jours est figé dans le code. Si le relevé compte six jours au lieu de sept, il faut supprimer une ligne et corriger le 7 de la division. Deux modifications pour un seul changement: c'est exactement la situation où l'on en oublie une.
Troisième défaut: la répétition appelle l'erreur. Vous avez écrit ces lignes par copier-coller. Il suffit d'oublier de changer un 4 en 5 pour que le programme demande deux fois le jour 4, et rien dans son exécution ne le signalera: il affichera un total parfaitement plausible et parfaitement faux. C'est la pire espèce de bogue, celle qui ne provoque aucune erreur.
Ce que l'on veut dire à la machine
Relisez ces sept lignes à voix haute et vous entendrez ce que vous vouliez réellement exprimer: «tant que je n'ai pas traité les sept jours, demande la température du jour courant, ajoute-la au total, et passe au jour suivant». Il y a là une phrase, pas sept. Elle contient une condition («tant que…»), un travail à répéter («demande… ajoute…») et un mouvement («passe au jour suivant»).
Une boucle est la construction qui permet d'écrire cette phrase telle quelle.
Voici le même programme avec une boucle. Comparez-le ligne à ligne au précédent.
# Le meme travail, ecrit une seule fois
total = 0.0
jour = 1 # initialisation
while jour <= 7: # condition
message = "Jour " + str(jour) + " - temperature: "
total = total + float(input(message))
jour = jour + 1 # progression
print("Total :", total)
print
Jour 1 - temperature: 2.4
Jour 2 - temperature: 5.1
Jour 3 - temperature: 7.8
Jour 4 - temperature: 6.2
Jour 5 - temperature: 3.9
Jour 6 - temperature: -0.5
Jour 7 - temperature: 1.7
Total : 26.599999999999998
Moyenne : 3.8
Caractère pour caractère, la sortie est celle du programme précédent. Mais le programme, lui, a cessé de grandir avec les données: pour passer à trente jours, on remplace le 7 de la condition par 30 — et le 7 de la division, dont nous verrons bientôt comment se débarrasser aussi.
Notez str(jour): input() attend une chaîne de caractères, et l'on ne peut pas concaténer une chaîne et un entier. "Jour " + jour déclencherait une TypeError, exactement comme au chapitre 1. Le chapitre 5 introduira les chaînes de format (f"…"), qui rendront cette ligne bien plus lisible; d'ici là, la concaténation et str() font le travail.
La boucle while
Syntaxe
while condition:
instruction_1
instruction_2
instruction_apres_la_boucle
Trois éléments de syntaxe, tous déjà rencontrés au chapitre 2 avec if:
- le mot-clé
while(tant que), suivi d'une expression booléenne: n'importe quelle expression valantTrueouFalse, doncjour <= 7,note != -1,reste_du_travail and not fini; - les deux-points en fin de ligne, qui annoncent un bloc;
- l'indentation du corps, de quatre espaces par convention. Comme pour
if, l'indentation est la syntaxe: elle seule dit où le corps commence et où il finit.instruction_apres_la_boucle, revenue à la marge, ne fait pas partie du corps et ne s'exécutera qu'une seule fois, à la sortie.
Le test a lieu avant le corps
C'est le point que l'on comprend une fois pour toutes ou que l'on ne comprend jamais. Python évalue la condition, puis décide. Si elle est vraie, il exécute le corps en entier, puis revient évaluer la condition. Si elle est fausse, il saute par-dessus le corps et continue après la boucle.
Deux conséquences immédiates. D'abord, le corps peut ne jamais s'exécuter: si la condition est fausse dès le départ, la boucle est traversée sans qu'aucune instruction du corps ne soit lue.
n = 0
while n > 0: # la condition est fausse des le depart
print(n)
n = n - 1
print("Fin, n vaut", n)
Fin, n vaut 0
Aucun nombre n'a été affiché: zéro tour. Une boucle qui s'exécute zéro fois n'est pas un bogue, c'est un cas parfaitement normal et souvent le plus important à vérifier (une liste vide, un fichier sans ligne, un utilisateur qui abandonne tout de suite).
Ensuite, la condition n'est vérifiée qu'aux frontières des tours, jamais au milieu. Si le corps rend la condition fausse à sa deuxième ligne, les lignes suivantes s'exécutent quand même jusqu'au bout du tour. Python n'interrompt pas un corps de boucle en cours de route — sauf si vous le lui demandez explicitement avec break, que nous verrons plus loin.
Les trois pièces d'une boucle correcte
Toute boucle while juste comporte trois pièces. Il est utile de les nommer, parce que les boucles fausses sont presque toujours des boucles à qui il en manque une.
Dans le programme des températures, l'initialisation est jour = 1, la condition est jour <= 7, la progression est jour = jour + 1. Les trois portent sur la même variable jour, et c'est le cas le plus fréquent: une variable, souvent appelée variable de contrôle, qui est mise en place, testée et modifiée.
La boucle infinie
Une variable vaut . On exécute la boucle dont la condition est « est strictement inférieur à » et dont le corps est la seule instruction «ajouter à ». Combien de fois le corps s'exécute-t-il?
Compteurs et accumulateurs
Deux motifs suffisent à écrire l'immense majorité des boucles utiles. Ils se ressemblent tellement qu'on les confond, et ils diffèrent sur un point essentiel: ce que l'on met dans la variable.
Le compteur est donc un accumulateur particulier, celui qui accumule des 1. Mais il vaut la peine de les distinguer, car ce qu'ils répondent n'est pas de même nature: un accumulateur répond «combien au total», un compteur répond «combien de fois».
L'accumulateur de somme
Prenons le carnet de notes du cours: dix étudiants fictifs, notés de 1 à 6 selon l'usage suisse, avec 4 comme note suffisante. Leurs dix notes, qui reviendront dans tout le cours, sont 4,5, 5,0, 3,5, 6,0, 4,0, 5,5, 4,5, 3,0, 5,0 et 4,5. Nous les saisissons une par une; le chapitre 6 les rangera dans une liste et le chapitre 7 les associera aux noms.
# Accumulateur: le total des dix notes du cours
total = 0.0 # element neutre de l'addition
numero = 1
while numero <= 10:
note = float(input("Note " + str(numero) + ": "))
total = total + note # on accumule
numero = numero + 1
print("Somme :", total)
Note 1: 4.5
Note 2: 5.0
Note 3: 3.5
Note 4: 6.0
Note 5: 4.0
Note 6: 5.5
Note 7: 4.5
Note 8: 3.0
Note 9: 5.0
Note 10: 4.5
Somme : 45.5
La ligne total = total + note est la ligne clé, et elle mérite qu'on s'y arrête, parce qu'elle est mathématiquement absurde et informatiquement limpide. En mathématiques, n'a de solution que si . En Python, = n'est pas une égalité mais une affectation, comme le chapitre 1 l'a martelé: le membre de droite est d'abord calculé avec les valeurs actuelles, puis le résultat est rangé dans le nom de gauche. On lit donc «le nouveau total est l'ancien total augmenté de note». Python offre l'abréviation total += note, strictement équivalente; nous écrirons la forme longue dans ce chapitre, parce qu'elle montre le mécanisme.
Lisez cette table de haut en bas et vous verrez la boucle travailler. Une colonne ne bouge que d'une unité à la fois (numero), une colonne saute d'une valeur à l'autre sans logique apparente (note, c'est la donnée), et une colonne croît régulièrement (total, c'est le résultat en construction). Dresser cette table à la main, sur trois ou quatre tours, est le meilleur outil de déverminage qui existe, et il ne demande qu'un crayon. Chaque fois qu'une boucle vous résiste, faites-le.
L'accumulateur de produit
Même motif, autre opération. La factorielle se calcule ainsi:
# Accumulateur multiplicatif: 6! = 1 x 2 x 3 x 4 x 5 x 6
produit = 1 # element neutre de la multiplication
k = 1
while k <= 6:
produit = produit * k
k = k + 1
print("6! =", produit)
6! = 720
Initialiser produit à 0 aurait donné 0, définitivement: zéro absorbe tout ce qu'on lui multiplie. C'est l'erreur d'initialisation la plus facile à diagnostiquer, parce que le résultat est spectaculairement faux.
Le compteur
Combien d'étudiants ont la moyenne, c'est-à-dire une note d'au moins 4? Ici on ne cumule pas les notes, on compte les cas.
# Compteur: combien de notes atteignent 4 ?
reussites = 0
numero = 1
while numero <= 10:
note = float(input("Note " + str(numero) + ": "))
if note >= 4:
reussites = reussites + 1 # on ne compte que si la condition est vraie
numero = numero + 1
print
Note 1: 4.5
Note 2: 5.0
Note 3: 3.5
Note 4: 6.0
Note 5: 4.0
Note 6: 5.5
Note 7: 4.5
Note 8: 3.0
Note 9: 5.0
Note 10: 4.5
Notes superieures ou egales a 4 : 8 sur 10
Huit sur dix, soit 80 %: ce sont les chiffres fixés du cours, et vous les retrouverez à l'identique aux chapitres 6 et 7. Observez la structure: une condition if à l'intérieur du corps de la boucle, indentée d'un niveau supplémentaire. Le if décide à chaque tour; la boucle, elle, fait tous les tours. Remarquez aussi que reussites = reussites + 1 est dans le if alors que numero = numero + 1 est hors du if: si l'on indentait la progression d'un cran de trop, elle ne s'exécuterait que pour les notes suffisantes et la boucle deviendrait infinie dès la première note en dessous de 4. L'indentation n'est pas de la mise en page.
Maximum et minimum: la question de l'initialisation
Chercher la plus grande valeur suit le même schéma: une variable qui retient «le meilleur vu jusqu'ici», comparée à chaque nouvelle valeur. Le motif s'écrit tout seul; c'est l'initialisation qui pose un vrai problème.
Voici la version naïve, appliquée aux températures de la semaine.
# ATTENTION: ce programme est FAUX. Le piege est l'initialisation a 0.
maximum = 0.0
jour = 1
while jour <= 7:
t = float(input("Jour " + str(jour) + " - temperature: "))
if t > maximum:
maximum = t
jour = jour + 1
print("Maximum :", maximum)
Sur la semaine du cours, il donne le bon résultat:
Jour 1 - temperature: 2.4
Jour 2 - temperature: 5.1
Jour 3 - temperature: 7.8
Jour 4 - temperature: 6.2
Jour 5 - temperature: 3.9
Jour 6 - temperature: -0.5
Jour 7 - temperature: 1.7
Maximum : 7.8
Le maximum vaut bien 7,8 °C. Le programme est faux quand même, et la semaine du cours contient déjà l'indice de sa fausseté: le jour 6 est à −0,5 °C. Les températures peuvent être négatives; rien ne garantit donc qu'il existe une valeur supérieure à 0. Essayons le même programme sur une autre semaine, fictive elle aussi, entièrement en dessous de zéro — un relevé de janvier: −4,1, −2,6, −6,3, −1,2, −3,8, −5,5 et −0,9.
Jour 1 - temperature: -4.1
Jour 2 - temperature: -2.6
Jour 3 - temperature: -6.3
Jour 4 - temperature: -1.2
Jour 5 - temperature: -3.8
Jour 6 - temperature: -5.5
Jour 7 - temperature: -0.9
Maximum : 0.0
Le programme annonce un maximum de 0,0 °C. Aucune des sept journées n'a connu cette température. La valeur affichée n'est pas une donnée, c'est la valeur d'initialisation qui a survécu: la condition t > maximum n'a jamais été vraie, et maximum est resté ce qu'il était au départ.
Le même code, avec if t < minimum: et une deuxième variable, donne le minimum. On peut naturellement faire les deux dans une seule boucle: le corps peut contenir autant d'accumulateurs et de compteurs que l'on veut, et il est beaucoup plus efficace de tout calculer en un seul parcours qu'en trois.
# La premiere valeur sert d'initialisation: elle est forcement dans les donnees
t = float(input("Jour 1 - temperature: "))
maximum = t
minimum = t
jour = 2
while jour <= 7:
t = float(input("Jour " + str(jour) + " - temperature: "))
if t > maximum:
maximum =
Jour 1 - temperature: 2.4
Jour 2 - temperature: 5.1
Jour 3 - temperature: 7.8
Jour 4 - temperature: 6.2
Jour 5 - temperature: 3.9
Jour 6 - temperature: -0.5
Jour 7 - temperature: 1.7
Maximum : 7.8
Minimum : -0.5
Un accumulateur produit vaut avant la boucle. Le corps exécute produit = produit * 2 et la boucle fait cinq tours. Quelle valeur produit a-t-il à la sortie?
À vous, et cette fois la machine répond. Le squelette prépare l'accumulateur et affiche le résultat; écrivez la boucle qui manque, pour que somme contienne la somme des multiples de 7 strictement inférieurs à 100. Ctrl+Entrée exécute.
Invariant de boucle et terminaison
Nous savons écrire des boucles. Comment savoir qu'elles sont justes? Deux questions, et deux seulement, y répondent:
- Qu'est-ce qui reste vrai à chaque tour? C'est l'invariant. Il dit ce que la boucle a accompli jusqu'ici.
- Qu'est-ce qui diminue strictement à chaque tour? C'est le variant. Il dit que la boucle finira.
Ces deux mots ont l'air théoriques; ils sont en réalité très pratiques. L'invariant est la phrase que vous écrivez en commentaire au-dessus de la boucle pour vous rappeler ce que vous faites. Le variant est la réponse à «pourquoi cette boucle s'arrête-t-elle?», et vous l'avez déjà utilisé sans le nommer quand vous cherchiez la progression manquante d'une boucle infinie.
Un exemple complet: la factorielle
Reprenons la boucle de la factorielle et instrumentons-la, c'est-à-dire ajoutons-y des print qui affichent l'état des variables avant chaque test. C'est la seule façon honnête de produire une trace: on ne la devine pas, on la fait imprimer par le programme. Un print avant la boucle donne l'état du premier test; un print à la fin du corps donne l'état de chacun des suivants, y compris celui qui arrêtera la boucle.
# Version instrumentee: on affiche l'etat AVANT chaque test
produit = 1
k = 1
tour = 0
print(tour, k, produit, 7 - k, k <= 6) # etat avant le premier test
while k <= 6:
produit = produit * k
k = k + 1
tour = tour + 1
print(tour, k, produit, 7
0 1 1 6 True
1 2 1 5 True
2 3 2 4 True
3 4 6 3 True
4 5 24 2 True
5 6 120 1 True
6 7 720 0 False
Resultat : 720
La table ci-dessous est exactement cette sortie, mise en forme, avec une colonne de plus pour la vérification.
| Avant le test no | k | produit | k <= 6 | ||
|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 6 | vrai |
| 2 | 2 | 1 | 1 | 5 | vrai |
| 3 | 3 | 2 | 2 | 4 | vrai |
| 4 | 4 | 6 | 6 | 3 | vrai |
| 5 | 5 | 24 | 24 | 2 | vrai |
| 6 | 6 | 120 | 120 | 1 | vrai |
| 7 | 7 | 720 | 720 | 0 | faux |
L'invariant saute aux yeux en comparant les colonnes produit et : elles sont égales sur les sept lignes. Autrement dit,
Vérifions-le comme on vérifie une récurrence. Au départ, et produit : l'invariant est vrai avant le premier test. À chaque tour, si l'invariant est vrai avec la valeur , le corps calcule produit * k puis remplace par ; l'invariant, réécrit avec la nouvelle valeur de , redevient : il est donc encore vrai avant le test suivant. , la condition est fausse, donc ; comme augmente de 1 par tour en partant de 1, la première valeur qui sort est , et l'invariant donne . C'est le résultat annoncé, et il est , pas testé.
Le variant est la colonne : 6, 5, 4, 3, 2, 1, 0. Elle est entière, positive ou nulle tant que la boucle tourne, et elle diminue exactement de 1 à chaque tour puisque augmente de 1. Une quantité entière positive qui décroît strictement ne peut le faire qu'un nombre fini de fois: la boucle termine, et l'on sait même en combien de tours, à savoir la valeur initiale du variant, ici 6.
Il existe des boucles dont on ne sait pas prouver la terminaison. La plus célèbre tient en quatre lignes et fait l'objet d'une conjecture ouverte depuis 1937; c'est l'exercice 3.5. Le fait qu'une boucle s'arrête sur tous les cas que vous avez essayés ne prouve rien du tout — c'est même, en informatique, le prototype de ce qui ne se prouve pas par l'exemple.
Remettez dans l'ordre les étapes de l'exécution d'une boucle while qui calcule la somme des entiers de à .
Glissez les éléments pour les mettre dans le bon ordre
- Revenir au test avec la nouvelle valeur de k
- La condition devient fausse: sortir de la boucle et afficher total
- Initialiser total à 0 et k à 1, avant la boucle
- La condition est vraie: entrer dans le corps et ajouter k à total
- Faire progresser k d'une unité
- Évaluer la condition: k ne dépasse-t-il pas 100?
La boucle for et range
Compter, c'est si fréquent que Python a une syntaxe pour ça
Toutes les boucles précédentes ont la même charpente: une variable de contrôle initialisée, testée, incrémentée. Ce motif est si courant que Python offre une construction qui l'écrit d'un coup, sans que l'on puisse oublier ni l'initialisation ni la progression.
for i in range(5):
print(i)
0
1
2
3
4
On lit: «pour chaque valeur i de range(5), faire…». La variable i est créée par le for, prend successivement chacune des valeurs, et le nombre de tours est décidé d'avance. Il n'y a plus de progression à écrire, donc plus de progression à oublier: une boucle for sur un range ne peut pas être infinie. C'est une raison suffisante de la préférer chaque fois qu'elle s'applique.
# Les trois formes de range, affichees sur une ligne chacune
ligne = ""
for i in range(4):
ligne = ligne + str(i) + " "
print("range(4) :", ligne)
ligne = ""
for i in range(2, 7):
ligne = ligne + str(i) + " "
range(4) : 0 1 2 3
range(2, 7) : 2 3 4 5 6
range(1, 10, 3) : 1 4 7
Au passage, vous venez de voir un troisième accumulateur: ligne, initialisé à la chaîne vide "" et allongé à chaque tour. C'est exactement le motif de la somme, avec la concaténation à la place de l'addition et "" à la place de 0. Les chaînes de caractères sont le sujet du chapitre 5; ici elles ne servent qu'à afficher plusieurs valeurs sur une seule ligne.
La convention de borne exclue
Cette convention n'est pas une brimade: elle évite plus d'erreurs qu'elle n'en cause. Trois arguments, dans l'ordre d'importance.
Le nombre de valeurs est une soustraction. range(a, b) produit exactement valeurs. range(3, 10) en produit sept, sans qu'aucun «plus un» ou «moins un» n'intervienne. Si la borne était incluse, il faudrait écrire partout, et c'est précisément le genre de petit calcul que l'on rate.
Deux intervalles consécutifs se recollent sans trou ni recouvrement. range(0, 5) et range(5, 10) couvrent ensemble range(0, 10), chaque entier exactement une fois. Avec des bornes incluses, il faudrait 0 à 4 puis 5 à 9, et c'est vous qui devriez penser au 4.
Un intervalle vide s'écrit naturellement. range(3, 3) ne produit rien, et c'est ce que l'on veut dire par «aucun élément». Avec une borne incluse, range(3, 2) serait la seule façon d'écrire l'ensemble vide, ce qui est déroutant.
ligne = ""
for i in range(10, 0, -2): # de 10 vers 0 exclu, de deux en deux
ligne = ligne + str(i) + " "
print("range(10, 0, -2) :", ligne)
ligne = ""
for i in range(3, 3): # borne de depart = borne d'arrivee
ligne = ligne
range(10, 0, -2) : 10 8 6 4 2
range(3, 3) : (rien)
range(0, 10, -1) : (rien)
Deux remarques sur ce dernier bloc. range(10, 0, -2) s'arrête à 2 et ne produit pas 0: la borne d'arrivée reste exclue, même en descendant. Pour descendre de 10 à 0 inclus, il faut range(10, -1, -1). Et range(0, 10, -1) ne produit rien du tout, sans lever d'erreur: on demande de partir de 0 en reculant jusqu'à 10, ce qui est impossible, et Python répond par zéro tour plutôt que par un message.
Le seul cas où range refuse: un pas nul
Un pas nul produirait une boucle infinie sur place, et Python le refuse.
debut = 0
fin = 10
pas = 0
for i in range(debut, fin, pas):
print(i)
Traceback (most recent call last):
File "semaine.py", line 4, in <module>
for i in range(debut, fin, pas):
~~~~~^^^^^^^^^^^^^^^^^
ValueError: range() arg 3 must not be zero
Lisons ce message ligne par ligne, comme l'annexe B vous apprendra à le faire systématiquement (le chemin complet du fichier est remplacé ici par son seul nom, pour la lisibilité).
Traceback (most recent call last): «voici la pile des appels, le plus récent en dernier». Il faut donc lire de bas en haut pour trouver la cause, et regarder d'abord la dernière ligne.File "semaine.py", line 4, in <module>: le fichier, le numéro de ligne, et l'endroit du programme (<module>signifie «au niveau principal du fichier», pas à l'intérieur d'une fonction).- La ligne fautive est recopiée, et les
~~~~^^^^soulignent l'expression exacte qui a échoué: ici l'appelrange(debut, fin, pas), pas lefor. ValueError: range() arg 3 must not be zero: le type de l'erreur, puis le message.ValueErrorsignifie que l'argument avait le bon type (un entier) mais une valeur inacceptable. Le troisième argument, c'est le pas.
Retenez la lecture: dernière ligne d'abord (quoi), numéro de ligne ensuite (où), le reste au besoin (comment on en est arrivé là).
Le programme simulé est toujours le même: total = 0, puis « for i in range(début, fin, pas): total = total + i ». Déplacez les curseurs et lisez, sur la règle, les entiers effectivement produits, puis, dans la table, la valeur de i et de total à la fin de chaque tour. Les deux cas dégénérés — un pas nul, que Python refuse, et un intervalle vide, où le corps n'est jamais exécuté — sont annoncés explicitement au lieu de produire une table muette.
L'explorateur ci-dessus rejoue toujours le même programme — total = 0, puis for i in range(début, fin, pas): total = total + i — avec les bornes et le pas que vous choisissez. Trois expériences valent la peine d'être faites:
- Fixez le pas à 1 et faites varier
fin: le nombre de tours affiché vaut toujoursfin - début. C'est la soustraction annoncée plus haut, et elle reste juste lorsque l'intervalle devient vide. - Amenez
débutau-dessus definavec un pas positif: zéro tour,totalreste à 0, et le diagnostic dit «intervalle vide». Aucune erreur — c'est le piège silencieux. - Mettez le pas à 0: l'explorateur affiche le
ValueErrorque Python produirait. C'est le seul des trois cas que Python refuse d'exécuter.
Quelle est la dernière valeur produite par range(2, 9, 3)?
La table demandée est la table de 7, fournie par la saisie. Écrivez la boucle qui affiche les dix lignes, sous la forme exacte 7 x 3 = 21.
Choisir entre for et while
La règle tient en une ligne, et elle ne souffre guère d'exception.
Cas pour un for. «Traiter les sept jours de la semaine», «afficher la table de 7», «répéter dix fois», «parcourir les entiers de 1 à 100». Le compte est connu; la boucle for le rend explicite et interdit la boucle infinie.
Cas pour un while. «Demander des notes jusqu'à ce que l'utilisateur tape une valeur d'arrêt», «diviser par 2 jusqu'à atteindre 1», «raffiner une approximation jusqu'à ce que l'écart soit inférieur au millième», «lire le fichier jusqu'à sa fin». Dans tous ces cas, le nombre de tours n'est pas connu au départ: il dépend des données.
Le premier de ces cas porte un nom.
break et continue
Deux instructions modifient le déroulement d'une boucle depuis l'intérieur de son corps. Elles fonctionnent aussi bien avec for qu'avec while.
break: arrêter dès que l'on a trouvé
Cherchons le premier jour de gel de la semaine. Une fois qu'on l'a trouvé, continuer à lire est inutile.
# Trouver le premier jour de gel et s'arreter la
premier_gel = 0
for jour in range(1, 8):
t = float(input("Jour " + str(jour) + " - temperature: "))
if t < 0:
premier_gel = jour
break # inutile de lire la suite
if premier_gel == 0:
print
Jour 1 - temperature: 2.4
Jour 2 - temperature: 5.1
Jour 3 - temperature: 7.8
Jour 4 - temperature: 6.2
Jour 5 - temperature: 3.9
Jour 6 - temperature: -0.5
Premier jour de gel : jour 6
Regardez la sortie: le jour 7 n'a jamais été demandé. Le break a coupé la boucle au sixième tour. C'est l'usage exemplaire de break, celui d'une recherche: on parcourt jusqu'à trouver, et trouver rend la suite du parcours inutile.
Notez la variable premier_gel, initialisée à 0 — une valeur impossible pour un numéro de jour, donc utilisable comme marqueur de «pas trouvé». Après la boucle, elle répond à la question «a-t-on trouvé, et où?». Ce petit motif, une variable témoin initialisée à une valeur impossible, revient constamment.
continue: sauter un tour
Calculons la moyenne des seules notes suffisantes.
# Moyenne des seules notes suffisantes
total = 0.0
combien = 0
for numero in range(1, 11):
note = float(input("Note " + str(numero) + ": "))
if note < 4:
continue # on passe directement au tour suivant
total = total + note
combien =
Note 1: 4.5
Note 2: 5.0
Note 3: 3.5
Note 4: 6.0
Note 5: 4.0
Note 6: 5.5
Note 7: 4.5
Note 8: 3.0
Note 9: 5.0
Note 10: 4.5
Notes retenues : 8
Moyenne des notes suffisantes : 4.875
Les dix notes ont bien été lues — continue n'arrête pas la boucle, il abrège le tour — mais seules huit ont été accumulées, et leur moyenne vaut 4,875, nettement au-dessus de la moyenne générale de 4,55: c'est mécanique, on a retiré les deux plus basses.
Quand ils clarifient, et quand ils obscurcissent
Voici la vérité désagréable: break et continue sont à la fois très utiles et très faciles à mal employer. Ils cassent l'idée même d'une boucle dont on lit la condition en tête pour savoir quand elle s'arrête. Quelques repères honnêtes.
Ils clarifient quand:
breaktermine une recherche. «Parcourir jusqu'à trouver» est un énoncé naturel, et l'alternative — une variable booléennetrouveajoutée à la condition — est en général plus longue et moins lisible. Comparez la version avecbreakci-dessus à celle-ci, qui fait la même chose:
premier_gel = 0
jour = 1
while jour <= 7 and premier_gel == 0: # deux conditions a suivre
t = float(input("Jour " + str(jour) + " - temperature: "))
if t < 0:
premier_gel = jour
jour = jour + 1
La condition porte maintenant sur deux choses, et il faut vérifier que premier_gel est bien mis à jour partout. La version avec break dit exactement ce qu'elle fait.
breakévite la lecture anticipée. Le doublon de saisie de l'exemple 3.2 disparaît si l'on écrit la boucle sous la formewhile True:avec unbreakjuste après la lecture. On accepte alors une boucle dont la condition n'apprend rien (True) contre une lecture écrite une seule fois. C'est un arbitrage courant, et les deux versions se défendent.continueremplace unifqui envelopperait tout le corps. Si le corps entier serait indenté dans unif, uncontinueau début aplatit le code d'un niveau. Sur un corps de quinze lignes, c'est un vrai gain.
Ils obscurcissent quand:
- il y en a plusieurs. Trois
breakdans une même boucle, c'est trois sorties à retrouver avant de comprendre quand elle s'arrête. À partir de deux, demandez-vous si la condition ne dirait pas mieux les choses. - ils sont enfouis. Un
breakà la dix-septième ligne d'un corps de vingt, à l'intérieur de deuxifimbriqués, est invisible. Une boucle dont on ne voit pas les sorties d'un coup d'œil est une boucle que l'on ne comprend pas. - on les emploie par paresse.
continueutilisé pour éviter d'écrire une condition correcte finit par dissimuler la vraie règle de sélection. Unif note >= 4:autour de deux lignes dit la même chose plus directement queif note < 4: continue; sur un corps court, préférez leif. - on croit que
breaksort de tout.breakne sort que de la boucle qui le contient immédiatement. Dans une boucle imbriquée, il quitte la boucle interne et l'externe continue. C'est la source d'un bogue classique, et la section suivante l'illustre.
Une boucle parcourt range(1, 21) et accumule dans total les seuls entiers divisibles par , les autres étant sautés par un continue. Quelle valeur total a-t-il à la fin?
Boucles imbriquées
Le corps d'une boucle est un bloc d'instructions comme un autre; il peut donc contenir une boucle. On parle alors de boucles imbriquées (nested loops), et la règle d'exécution est simple: pour chaque tour de la boucle externe, la boucle interne fait tous ses tours, du début à la fin.
Le même mécanisme dessine des figures de caractères, un exercice classique parce qu'il rend le nombre de tours visible à l'œil:
hauteur = 5
for ligne in range(1, hauteur + 1):
etoiles = ""
for k in range(ligne): # ligne etoiles au tour numero ligne
etoiles = etoiles + "*"
print(etoiles)
*
**
***
****
*****
La boucle interne ne fait pas toujours le même nombre de tours: elle en fait ligne, c'est-à-dire 1, puis 2, puis 3, 4 et 5. Le total vaut tours de la boucle interne, et non . Remarquez aussi range(1, hauteur + 1): le + 1 visible dont il était question plus haut, pour aller de 1 à hauteur inclus.
Le coût: tours
Une dernière mise en garde, annoncée plus haut: dans une boucle imbriquée, break ne sort que de la boucle interne. Si vous cherchez une valeur dans un tableau à deux dimensions et que vous voulez tout arrêter, un seul break ne suffit pas; il en faut un second dans la boucle externe, avec une variable témoin pour le déclencher. Le chapitre 4 offrira la solution élégante: un return depuis une fonction sort de tout d'un coup.
Deux programmes complets
Rassemblons tout. Les deux programmes qui suivent sont ceux que vous devriez savoir écrire à la fin de ce chapitre; ils n'utilisent que for, range, un if, des accumulateurs et des compteurs — pas de liste, pas de fonction.
Le carnet de notes
# Le carnet de notes du cours (donnees fictives): moyenne, reussites, extremes
total = 0.0
reussites = 0
maximum = 0.0
minimum = 0.0
for numero in range(1, 11):
note = float(input("Note " + str(numero) + ": "))
total = total + note
if note >=
Une fois la boucle terminée, trois lignes d'affichage suffisent:
print("Moyenne :", total / 10)
print("Notes suffisantes :", reussites, "sur 10")
print("Meilleure :", maximum, "- plus basse :", minimum)
Note 1: 4.5
Note 2: 5.0
Note 3: 3.5
Note 4: 6.0
Note 5: 4.0
Note 6: 5.5
Note 7: 4.5
Note 8: 3.0
Note 9: 5.0
Note 10: 4.5
Moyenne : 4.55
Notes suffisantes : 8 sur 10
Meilleure : 6.0 - plus basse : 3.0
Moyenne 4,55; huit notes suffisantes sur dix; meilleure 6,0 (David); plus basse 3,0 (Hugo). Ce sont exactement les chiffres fixés pour ce cours, et ils réapparaîtront tels quels aux chapitres 6, 7 et 10.
Deux remarques techniques. La ligne if numero == 1: est la façon de faire «initialiser avec la première valeur» à l'intérieur d'une boucle qui lit tout, y compris le premier élément: on repère le premier tour par son numéro. Elle est placée avant les deux comparaisons, de sorte que la première note se compare à elle-même, ce qui est sans effet. Et la division par 10 reste écrite en dur; c'est acceptable ici parce que la boucle elle-même est écrite pour dix notes, mais un compteur combien serait plus robuste — c'est ce que fait le programme à sentinelle.
La semaine de températures
Le second programme fait la même chose sur les sept températures, et compte en plus les jours de gel. Il est en deux morceaux: l'initialisation à partir du premier jour, puis la boucle sur les six autres.
# La semaine de temperatures (donnees fictives): moyenne, extremes, gel
premiere = float(input("Jour 1 - temperature: "))
total = premiere
maximum = premiere
minimum = premiere
jours_de_gel = 0
if premiere < 0:
jours_de_gel = 1
for jour in range(2, 8):
t = float(input("Jour " + str(jour) + " - temperature: "))
total = total + t
if t > maximum:
maximum = t
if t < minimum:
minimum = t
if t <
Jour 1 - temperature: 2.4
Jour 2 - temperature: 5.1
Jour 3 - temperature: 7.8
Jour 4 - temperature: 6.2
Jour 5 - temperature: 3.9
Jour 6 - temperature: -0.5
Jour 7 - temperature: 1.7
Moyenne : 3.8
Maximum : 7.8 - minimum : -0.5
Jours de gel : 1
Moyenne 3,8 °C — soit 3,80 °C au centième —, maximum 7,8 °C, minimum −0,5 °C, un seul jour de gel. Ce sont les valeurs fixées du cours, et elles sortent bien du programme.
Notez range(2, 8): six tours, du jour 2 au jour 7 inclus, puisque la borne 8 est exclue. C'est exactement le genre d'endroit où l'on écrit range(2, 7) par distraction et où le programme perd silencieusement le dernier jour — la moyenne deviendrait alors , un chiffre assez plausible pour ne pas éveiller les soupçons. C'est pour cela que l'on vérifie un programme sur des données dont on connaît déjà la réponse.
Synthèse
- Une boucle exprime «faire ceci plusieurs fois» en une seule écriture du travail à faire; sans elle, la longueur du programme croît avec la quantité de données, et la répétition du code est un terrain de culture pour les erreurs silencieuses.
- Une boucle
whilecorrecte comporte trois pièces: initialisation, condition testée avant chaque tour, progression vers la fausseté de la condition. La progression manquante ou inversée est la cause de presque toutes les boucles infinies; préférez une comparaison d'ordre à une comparaison d'égalité. - Les deux motifs fondamentaux sont l'accumulateur, initialisé à l'élément neutre de son opération (
0pour une somme,1pour un produit,""pour un texte), et le compteur, initialisé à0et incrémenté sous condition. Un maximum ou un minimum s'initialise avec la première valeur des données, jamais avec0: sur des températures négatives,0renvoie une valeur qui n'a jamais été mesurée. - L'invariant dit ce qui est vrai à chaque tour et, joint à la négation de la condition, donne le résultat; le variant, quantité entière positive qui décroît strictement, prouve la terminaison. Une trace obtenue en instrumentant le programme est le premier outil de déverminage.
foravecrangefixe le nombre de tours d'avance et ne peut pas boucler indéfiniment: utilisez-le dès que ce nombre est connu, et gardezwhilepour les arrêts décidés par les données (sentinelle, précision atteinte, fin de fichier).range(n)s'arrête àn - 1; pour aller deaàbinclus, écrivezrange(a, b + 1).breaksort de la boucle,continueabrège le tour: excellents pour une recherche ou pour aplatir un corps trop indenté, nuisibles dès qu'ils se multiplient ou s'enfouissent. Dans des boucles imbriquées, le corps interne s'exécute fois etbreakne quitte que la boucle interne.
Combien de valeurs range(3, 12, 4) produit-il?
On veut écrire, en quatre étapes, le programme qui lit les sept températures de la semaine du cours (2,4, 5,1, 7,8, 6,2, 3,9, −0,5 et 1,7 °C, données fictives) et affiche la moyenne, le maximum, le nombre de jours de gel et le nombre de jours au-dessus de la moyenne. Chaque étape ajoute une brique à la précédente, et chaque brique est un compteur ou un accumulateur.
La boucle de lecture
Il faut d'abord parcourir les sept jours. Le nombre de tours est connu: c'est donc un for sur un range.
Quel appel à range fait exactement sept tours, avec une variable jour valant successivement ?
L'accumulateur de somme
Le compteur de jours de gel
Deux parcours ou un seul?
Exercices
Vous pouvez afficher le corrigé directement sous chaque énoncé après avoir cherché la solution.
Écrivez un programme qui demande un entier à l'utilisateur et affiche sa table de multiplication de 1 à 10, une ligne par produit, sous la forme 7 x 3 = 21. Vérifiez-le sur 7.
Solution
Le nombre de tours est connu d'avance — dix — donc c'est un for sur un range. Pour aller de 1 à 10 inclus, la borne d'arrivée est 11.
# Table de multiplication d'un nombre
n = int(input("Quelle table ? "))
for i in range(1, 11):
print(n, "x", i, "=", n Écrivez un programme qui calcule, sans aucune saisie, la somme de tous les entiers strictement inférieurs à 100 qui sont divisibles par 3 ou par 5, et qui affiche aussi combien il y en a. Utilisez l'opérateur % du chapitre 1 et le or du chapitre 2.
Solution
Un compteur et un accumulateur dans la même boucle, avec un if qui sélectionne.
# Somme des entiers de 1 a 99 divisibles par 3 ou par 5
total = 0
combien = 0
for k in range(1, 100):
if k % 3 == 0 or k %
L'algorithme d'Euclide dans sa version la plus ancienne calcule le plus grand commun diviseur de deux entiers positifs en remplaçant répétitivement le plus grand des deux par leur différence, jusqu'à ce qu'ils soient égaux.
- Écrivez ce programme pour et , en comptant les tours.
- Donnez l'invariant de la boucle et le variant qui prouve qu'elle termine.
- Dressez la trace des variables tour par tour en instrumentant le programme.
Solution
1. La condition de continuation est «les deux valeurs diffèrent», donc un while: le nombre de tours n'est pas connu d'avance.
# PGCD par soustractions successives (algorithme d'Euclide, version lente)
a = 84
b =
Écrivez un programme qui demande un entier et dit s'il est premier, en affichant le cas échéant un diviseur. Arrêtez la recherche dès qu'un diviseur est trouvé, et n'essayez pas de diviseur au-delà de . Testez-le sur 97 et sur 91.
Solution
Le nombre de tours n'est pas connu d'avance — on s'arrête au premier diviseur — donc , avec un de recherche et une variable témoin.
On part d'un entier et l'on applique la règle suivante jusqu'à atteindre 1: si est pair, on le remplace par ; s'il est impair, par .
- Écrivez ce programme pour , en comptant les étapes et en retenant la plus grande valeur atteinte.
- Qu'est-ce qui empêche de donner un variant pour cette boucle?
Solution
Références
- Downey, A., Think Python, 3e éd., O'Reilly, Sebastopol, chap. 6 («Loops and Strings») et chap. 7 («Iteration and Search»). Librement disponible en ligne; une traduction française existe.
- Swinnen, G., Apprendre à programmer avec Python 3, Eyrolles, Paris, chap. 4 («Instructions répétitives»). Librement disponible en ligne.
- Matthes, E., Python Crash Course, 3e éd., No Starch Press, San Francisco, chap. 4 et 7 (boucles
for,whileet saisie utilisateur). - Guttag, J. V., Introduction to Computation and Programming Using Python, 3e éd., MIT Press, Cambridge, chap. 2 et 3, pour les notions de terminaison et de décrément de boucle.
- La documentation officielle Python, The Python Tutorial, section «More Control Flow Tools» (
for,range,break,continue,elsesur les boucles), sur docs.python.org.