La boucle for
Rappel d'ouverture (5 minutes, cours fermé)
Réponds sans rouvrir les pages précédentes, en écrivant tes réponses.
- Si
resvaut10, que vautresaprèsres = res + 3? Et dans quel ordre la machine procède-t-elle ? - Avec
mot = "python": que valentmot[0],mot[-1]etlen(mot)? Quel est le plus grand indice valide ? - Écris la condition qui teste que le caractère
cest une voyelle.
Corrigé
13. Le membre de droite est calculé d'abord (10 + 3), puis rangé dansres. C'est ce mécanisme, répété, qui va faire toute la page d'aujourd'hui."p","n"et6. Le plus grand indice valide est5, soitlen(mot) - 1: écriremot[6]lève uneIndexError.c in "aeiouy", ou une longue suite deor. La première écriture est préférable.
L'idée : pour chaque élément d'une séquence
Faire l'appel en classe, c'est parcourir la liste des élèves : pour chaque élève, on applique la même procédure.
C'est exactement ce que fait la boucle for en Python. Son idée tient en une phrase :
La boucle for
Pour chaque élément, qu'on appelle x, d'une séquence, répéter un bloc d'instructions (avec cette valeur de x).
À chaque tour, x prend la valeur de l'élément suivant de la séquence. Le bloc doit être indenté.
c, puis h, puis a, puis t : un tour par caractère.
Ce que fait la machine, tour par tour
Déroulons for x in [10, 20, 30]: avec, dans le corps, print(x) :
| Tour | x prend la valeur |
ce qui s'affiche |
|---|---|---|
| 1 | 10 |
10 |
| 2 | 20 |
20 |
| 3 | 30 |
30 |
Quand il n'y a plus d'élément, la boucle s'arrête. Le point clé, c'est de savoir ce que vaut x à chaque tour.
range : un intervalle semi-ouvert d'entiers
Très souvent, on veut parcourir des entiers. Python fournit pour cela range, qui est la séquence représentant un intervalle semi-ouvert d'entiers.
range(a, b) représente l'intervalle semi-ouvert \([a\,;\,b)\) : la borne a est incluse, la borne b est exclue. Parcourir un range avec for, c'est donc « pour chaque entier de l'intervalle » :
Deux conséquences de l'intervalle semi-ouvert
range(n)vautrange(0, n): la séquence0, 1, ..., n-1, soitnentiers en partant de 0.- Comme
best exclu,b - aest exactement le nombre d'entiers parcourus.
Pourquoi ce choix n'est pas anodin
Exclure la borne haute peut sembler arbitraire. C'est en réalité un choix de conception qui fait disparaître à l'avance les erreurs de « plus ou moins un » (les bugs les plus fréquents chez les débutants) :
- les indices d'une séquence de longueur
nsont exactementrange(n): la longueur est la borne exclue, doncrange(len(s))donne pile les bons indices, jamais un de trop ni un de moins ; - pour couper une séquence à la position
k, les deux morceauxrange(0, k)etrange(k, n)se recollent sans trou ni doublon ; - l'intervalle vide s'écrit simplement
range(a, a).
Ce n'est donc pas une bizarrerie de Python, mais une convention argumentée, défendue notamment par Edsger Dijkstra (Why numbering should start at zero, note EWD 831, 1982).
Répéter n fois
« Répéter n fois » est simplement le cas où on parcourt range(n) sans se soucier de l'élément. Par convention, on nomme alors la variable _ (souligné), pour dire « je n'utilise pas cette valeur ».
L'accumulation : le motif fondamental
La plupart des problèmes se résolvent en construisant progressivement un résultat dans une variable appelée l'accumulateur. C'est le motif le plus important du cours.
Exemple : la somme des entiers de 1 à n.
- On crée l'accumulateur. Une somme d'entiers est un entier, il vaut
0au départ (on n'a rien ajouté) : - On parcourt les entiers de
1àn, doncrange(1, n + 1): - À chaque entier rencontré, on l'ajoute au résultat :
- À la fin de la boucle, tous les entiers ont été ajoutés :
Méthodologie de l'accumulation
- Je réfléchis au type de mon résultat et je l'initialise correctement.
- Qu'est-ce que je dois parcourir ? J'écris correctement mon parcours.
- Dans le parcours, j'applique à chaque élément le comportement d'accumulation voulu par le problème.
Compter et filtrer : une condition dans la boucle
Souvent, on n'accumule que les éléments qui remplissent une condition : on place alors un if (voir Les conditionnelles) à l'intérieur de la boucle. Le point délicat n'est pas le if en lui-même, mais où placer chaque ligne.
Exemple : compter combien de fois le 6 sort sur 1000 lancers de dé.
from random import randint
compteur = 0 # AVANT la boucle (une seule fois)
for _ in range(1000): # 1000 tours
de = randint(1, 6)
if de == 6: # test à CHAQUE tour
compteur = compteur + 1
print(compteur) # APRÈS la boucle (une seule fois)
Le piège de l'emplacement
Trois choses vivent à trois endroits différents :
- l'initialisation de l'accumulateur : avant la boucle ;
- le test et la mise à jour : dans la boucle (indentés) ;
- l'affichage (ou le
return) du résultat : après la boucle.
Mettre l'initialisation dans la boucle la remettrait à zéro à chaque tour ; mettre l'affichage dans la boucle l'afficherait 1000 fois.
Trois sortes d'erreurs, et elles ne se cherchent pas de la même façon
Le piège ci-dessus appartient à la troisième catégorie, la plus coûteuse. Apprends à les distinguer dès maintenant : c'est ce qui sépare une méthode d'un tâtonnement.
| Type | Ce que tu observes | Ce que la machine te donne |
|---|---|---|
| Syntaxe | Le programme ne démarre pas | Un message et un numéro de ligne |
| Exécution | Il s'arrête en route (IndexError, TypeError...) |
Un message et l'endroit où il a cassé |
| Logique | Il tourne jusqu'au bout et donne un résultat faux | Rien du tout |
Les deux premières te disent où regarder. La troisième ne te dit rien : c'est celle qui exige une méthode, et c'est celle que produit une initialisation mal placée. Un accumulateur remis à zéro à chaque tour ne provoque aucune erreur, il donne juste un résultat faux, sans prévenir.
Les indices sont faits pour être ouverts
Ouvrir un indice n'est pas tricher, et ce n'est pas non plus un aveu. Ce qui coûte, ce n'est pas de demander de l'aide, c'est de rester bloqué vingt minutes sans rien produire, ou d'ouvrir la solution sans avoir compris ce qui bloquait.
La seule règle : quand un indice te débloque, écris en une phrase ce qu'il t'a appris sur ton erreur avant de continuer. C'est cette phrase qui reste, pas la solution recopiée.
Somme des pairs
Écris somme_pairs(n) qui renvoie la somme des entiers pairs de 1 à n inclus.
Indice léger
Reprends les trois temps de la méthodologie. Le filtre ne change ni l'initialisation ni le parcours : il ne change que ce qui se passe dans la boucle.
Indice plus précis
Un accumulateur res = 0 avant, un parcours for i in range(1, n + 1), et dans la boucle un if i % 2 == 0 avant d'ajouter.
Avant d'ouvrir la solution
Écris une phrase sur ton cahier : qu'est-ce que l'indice t'a appris sur ce qui bloquait dans ton code ?
Une phrase suffit, et elle doit parler de ton code, pas du cours. C'est ce geste qui fait la différence entre finir l'exercice et savoir le refaire seul la prochaine fois.
Deux traitements en un seul parcours : la fusion
Parfois, on veut deux résultats à la fois, calculés pendant le même parcours. On mène alors deux accumulateurs en parallèle dans une seule boucle. C'est la fusion, et c'est le point où l'on se trompe le plus souvent : on oublie d'en initialiser un, ou on met une mise à jour au mauvais endroit.
Exemple : compter les voyelles et les consonnes d'un texte. Deux comptages, un seul parcours :
def voyelles_et_consonnes(txt: str) -> tuple[int, int]:
"""Renvoie le nombre de voyelles et le nombre de consonnes de txt."""
voyelles = 0 # premier accumulateur
consonnes = 0 # second accumulateur
for c in txt:
if c in "aeiouy":
voyelles = voyelles + 1 # on met à jour l'UN
else:
consonnes = consonnes + 1 # ou l'AUTRE, dans la même boucle
return (voyelles, consonnes)
La fusion, à traiter pour elle-même
Additionner d'un côté, puis compter de l'autre, c'est facile. Les fusionner dans une seule boucle est une compétence à part : chaque accumulateur a sa propre initialisation (avant), sa propre mise à jour (dans), et le résultat combine les deux (après). Deux accumulateurs, une boucle.
Fusion : compter et construire en un seul parcours
Écris majuscules_et_compte(txt) qui renvoie le couple formé du texte en majuscules et du nombre de lettres converties, en un seul parcours.
Rappel : c.upper() renvoie la majuscule du caractère c, et c.islower() dit si c est une minuscule.
Indice léger
Deux accumulateurs de types différents : l'un est une chaîne, l'autre un entier. Quelle est la valeur initiale de chacun ?
Indice plus précis
res = "" et compte = 0 avant la boucle. Dans la boucle, on ajoute c.upper() à res à chaque tour, mais on n'incrémente compte que si c.islower().
Avant d'ouvrir la solution
Écris une phrase sur ton cahier : lequel de tes deux accumulateurs était mal placé, et pourquoi ?
Si tu ne peux pas répondre, c'est que tu n'as pas encore identifié ton erreur, et la solution ne te l'apprendra pas.
Solution
def majuscules_et_compte(txt: str) -> tuple[str, int]:
"""Renvoie txt en majuscules et le nombre de lettres converties."""
res = ""
compte = 0
for c in txt:
if c.islower():
compte = compte + 1
res = res + c.upper()
return (res, compte)
if. C'est exactement ce qui rend la fusion difficile.
Le même for sur les autres séquences
Pour l'instant, tu as parcouru deux choses : un range d'entiers et une chaîne de caractères. C'est déjà le même mécanisme, on change seulement ce qu'on parcourt.
Il y a alors deux façons de parcourir une séquence s.
Par élément (à privilégier, plus lisible) :
Par indice, quand on a besoin de la position. Les indices de s vont de 0 à len(s) - 1, donc on parcourt range(len(s)) :
Ce mécanisme resservira tel quel
Le for ne change pas selon ce qu'il parcourt. Quand tu rencontreras les listes, les tuples et les dictionnaires, tu n'auras aucune nouvelle boucle à apprendre : ce sera exactement ce for, sur une autre séquence. C'est l'un des rares endroits du cours où l'on apprend une chose une fois pour toutes.
Là encore, c'est un for each : « pour chaque indice i dans range(len(s)) ».
Avant chaque exercice, deux questions
- Ce que je parcours (quelle séquence ?) ;
- comment je le parcours (par élément ou par indice ?).
Lire et prédire avant d'écrire
Avant d'écrire une boucle, entraîne-toi à lire celles des autres et à prédire leur résultat. C'est la meilleure préparation à en écrire soi-même.
Prédire (1)
Que vaut res à la fin ? Suis-le tour par tour, puis exécute pour vérifier.
Réponse
10. res prend successivement 0, 1, 3, 6, puis 10 (on ajoute 1, 2, 3, 4).
Prédire, puis modifier
- Prédis l'affichage.
- Modifie une seule ligne pour que le mot s'affiche à l'endroit.
Réponse
nohtyp: chaque lettre est placée devant les précédentes, donc la chaîne est renversée.- Remplacer
res = lettre + resparres = res + lettre.
Exercices
1 - Parcours simple
Affiche une par une les lettres demot, par élément, puis par indice.
2 - Un caractère sur deux
3 - Nombre de voyelles
On veut compter : que vaut le résultat au départ ? Complète aussi le type de retour.
4 - Renverser
5 - Contient (sans l'opérateur in)
6 - Somme, produit, factorielle
somme(n): la somme des entiers de1àn.produit_impairs(n): le produit des entiers impairs de1àn(attention à l'initialisation de l'accumulateur !).factorielle(n): \(1 \times 2 \times \dots \times n\). Que vautfactorielle(0)avec ton code ?
Indice léger
Reprends la méthodologie de l'accumulation : de quel type est le résultat ? Quelle valeur initiale ne change rien à une addition ? à une multiplication ?
Indice plus précis
Pour la somme, l'accumulateur part de 0 et on fait res = res + x. Pour le produit, il part de 1 (car multiplier par 1 ne change rien) et on fait res = res * x.
Solution
def somme(n: int) -> int:
"""Renvoie la somme des entiers de 1 à n."""
res = 0
for i in range(1, n + 1):
res = res + i
return res
def produit_impairs(n: int) -> int:
"""Renvoie le produit des entiers impairs de 1 à n."""
res = 1
for i in range(1, n + 1):
if i % 2 == 1:
res = res * i
return res
factorielle(0) vaut 1 : la boucle for i in range(1, 1) ne fait aucun tour, l'accumulateur garde sa valeur initiale 1.
7 - Première lettre la plus petite (sans la fonction min)
Les caractères se comparent avec < selon l'ordre alphabétique : "a" < "b" vaut True.
def plus_petite_lettre(txt: str) -> str:
"""Renvoie la plus petite lettre de txt, qui n'est pas vide.
>>> plus_petite_lettre("python")
'h'
>>> plus_petite_lettre("a")
'a'
"""
assert len(txt) > 0, "le texte ne doit pas être vide"
...
Indice léger
L'accumulateur n'est pas un compteur ici : c'est la meilleure valeur rencontrée jusqu'ici. Par quoi l'initialiser ? Surtout pas par "", qui serait plus petit que tout.
Indice plus précis
On l'initialise avec txt[0], le seul candidat dont on soit sûr qu'il appartient au texte. C'est d'ailleurs à cela que sert l'assert : sans lui, txt[0] planterait sur une chaîne vide.
Solution
Tu retrouveras exactement cet algorithme sur les listes, puis sur les dictionnaires. Ce n'est pas trois algorithmes, c'est le même.8 - Problème : bin2dec
Écris bin2dec(txt) qui convertit une écriture binaire (une chaîne de 0 et de 1) en entier décimal.
Indications : on peut renverser la chaîne pour que l'indice de chaque chiffre corresponde à sa puissance de 2, puis calculer la somme des puissances de 2 par accumulation (parcours par indice).
9 - take : garder les n premiers
def take(n: int, s: str) -> str:
"""Renvoie les n premiers caractères de s.
Si s est plus courte que n caractères, renvoie s en entier.
>>> take(3, "python")
'pyt'
>>> take(0, "python")
''
>>> take(10, "abc")
'abc'
"""
...
Indice léger
C'est une accumulation de caractères, donc un parcours qui construit une chaîne. Mais tu ne veux pas tous les caractères : il te faut un filtre, comme pour Somme des pairs. Sur quoi porte-t-il ici ?
Indice plus précis
Parcours par indice (for i in range(len(s))), et le filtre compare i à n : tu n'ajoutes s[i] à l'accumulateur que si i < n. Le cas n plus grand que s se règle tout seul : la boucle s'arrête avant d'avoir jamais pu être fausse.
Avant d'ouvrir la solution
Écris une phrase sur ton cahier : pourquoi ce filtre n'a-t-il rien de spécial à faire pour le cas take(10, "abc") ?
Solution
def take(n: int, s: str) -> str:
"""Renvoie les n premiers caractères de s."""
res = ""
for i in range(len(s)):
if i < n:
res = res + s[i]
return res
n dépasse len(s), la condition i < n reste vraie jusqu'au dernier tour, donc tous les caractères sont pris. C'est le même bénéfice que range semi-ouvert (section plus haut) : les bornes se comportent bien sans qu'on ait à y penser.
10 - drop : jeter les n premiers
def drop(n: int, s: str) -> str:
"""Renvoie s privée de ses n premiers caractères.
Si s est plus courte que n caractères, renvoie la chaîne vide.
>>> drop(3, "python")
'hon'
>>> drop(0, "python")
'python'
>>> drop(10, "abc")
''
"""
...
Indice léger
Transfert proche de l'exercice précédent : même squelette, un seul symbole change dans le filtre.
Une propriété à vérifier, pas à admettre
Choisis plusieurs valeurs de n et plusieurs chaînes, et vérifie que take(n, s) + drop(n, s) redonne toujours s. C'est une spécification que tes deux fonctions doivent respecter ensemble, au sens de Spécification et tests : si l'égalité casse sur un exemple, l'une des deux fonctions a un bug, même si chacune passait ses propres doctests.
Le for s'arrête toujours : il fait un tour par élément d'une séquence finie. Quand on ne sait pas à l'avance combien de tours faire, on utilise l'autre boucle : La boucle non bornée while.