Maths Post-Bac Ouvrir l'app

Exercices corrigés — Algorithmique & programmation (Python)

Algorithmique · 18 exercices-types du palier socle

L1L2L3Maths ingénieurCAPES

Chaque exercice donne l'énoncé, des indices progressifs et la correction rédigée étape par étape. Ouvre les blocs seulement après avoir cherché.

Revoir le cours : Algorithmique & programmation (Python) Définitions, méthodes et exemples corrigés du chapitre.

Affecter n'est pas égaler : trace, x = x + 1, = contre ==

DémonstrationDifficulté 2/5

1. On considère le programme ci-dessous. Dresser son tableau de trace — les valeurs de x et de y après chacune des cinq affectations —, puis prédire ce qu'il affiche.

x = 3
y = x + 2
x = x * y
y = x - y
x = x + 1
print(x, y)

2. En mathématiques, l'équation x=x+1x=x+1 n'a aucune solution. Que fait pourtant l'instruction x = x + 1 ? Prédire l'affichage du programme suivant.

n = 10
n = n + 1
n = n * n
print(n)

3. Un élève veut ranger la valeur 55 dans x et écrit le programme ci-dessous. Qu'affiche-t-il, et pourquoi ? Le corriger. Que répond Python si l'on écrit if x = 5: pour tester l'égalité ?

x = 2
x == 5
print(x)

4. Prédire l'affichage du programme suivant, puis expliquer pourquoi b ne « suit » pas a.

a = 4
b = a
a = 7
print(a, b)
Indices (3)

Exécute les lignes une par une : à chaque affectation, calcule d'abord le membre de droite avec les valeurs actuelles, puis range le résultat sous le nom écrit à gauche.

À la ligne 4 du premier programme, x ne vaut plus 33 : c'est la valeur rangée à la ligne 3 qui compte.

= range une valeur ; == compare deux valeurs et produit True ou False sans rien modifier.

Correction détaillée
Ce qu'il faut voir

Le signe = de Python ne dit pas que deux choses sont égales : il ordonne de ranger une valeur. L'instruction x = expression s'exécute en deux temps, toujours dans cet ordre : Python calcule d'abord le membre de droite avec les valeurs actuelles des variables, puis il range le résultat sous le nom écrit à gauche, en écrasant la valeur qui s'y trouvait. On dit qu'on affecte la valeur à x.

Un programme est donc une suite d'états : après chaque ligne, chaque variable a une valeur précise, et la ligne suivante part de cet état-là, pas de l'état initial. Rien n'est « vrai en même temps » comme dans un système d'équations.

La comparaison, elle, s'écrit ==. Elle ne modifie rien : elle produit une valeur de vérité, True ou False, qu'on peut afficher ou tester.

👉 Pour prédire un programme, tu ne lis pas ses lignes comme des équations à résoudre ensemble : tu les exécutes une par une, en tenant à jour un tableau de trace. C'est le geste de base de tout ce chapitre.

Le tableau de trace, ligne par ligne

On exécute les lignes dans l'ordre et on note l'état après chacune ; un tiret signale une variable pas encore créée.

étape x y
avant — —
après la ligne 1 3 —
après la ligne 2 3 5
après la ligne 3 15 5
après la ligne 4 15 10
après la ligne 5 16 10

Deux lignes demandent de l'attention. À la ligne 3, x = x * y calcule 3×5=153\times5=15 avec les valeurs du moment, puis écrase le 33. À la ligne 4, y = x - y utilise donc le nouveau x : 15−5=1015-5=10, et non 3−5=−23-5=-2 — l'ancienne valeur 33 n'existe plus nulle part. La dernière ligne affiche les deux valeurs finales, séparées par une espace :

x = 3
y = x + 2
x = x * y
y = x - y
x = x + 1
print(x, y)
16 10

Vérification. On fait afficher l'état après chaque ligne : les cinq lignes obtenues doivent reproduire les cinq dernières lignes du tableau.

x = 3
print(x)
y = x + 2
print(x, y)
x = x * y
print(x, y)
y = x - y
print(x, y)
x = x + 1
print(x, y)
3
3 5
15 5
15 10
16 10

Elles les reproduisent. C'est la méthode à garder pour contrôler une trace faite à la main : instrumenter le programme, c'est-à-dire insérer des print, puis comparer.

x = x + 1 : une instruction, pas une équation

Lue comme une équation, x=x+1x=x+1 donne 0=10=1 : aucune solution. Lue comme Python la lit, la phrase est « calcule x + 1 avec la valeur actuelle de x, puis range le résultat dans x ». Le nom ne change pas, la valeur augmente de 11 : on dit qu'on incrémente x.

Dans le programme de la question, n vaut successivement 1010, puis 10+1=1110+1=11, puis 11×11=12111\times11=121 :

n = 10
n = n + 1
n = n * n
print(n)
121

Ce motif — une variable qui se met à jour à partir de sa propre valeur — est celui des compteurs (c = c + 1) et des accumulateurs (s = s + t) qu'on retrouve dans toutes les boucles du lot B. Python accepte l'abréviation n += 1, qui fait la même chose sur un nombre (sur une liste, t += [x] modifie la liste en place, comme append : voir l'exercice E6) ; ce chapitre garde la forme longue, qui montre l'ordre des opérations.

Vérification. Les deux mises à jour reviennent à calculer (10+1)2(10+1)^2 d'un seul coup :

print((10 + 1) ** 2)
121

Même résultat par un autre chemin.

= contre == : ranger ou comparer

La deuxième ligne du programme de l'élève est une comparaison : x == 5 calcule False (puisque x vaut 22), et ce booléen, rangé nulle part, est aussitôt perdu. Rien n'a été modifié, et le programme affiche l'ancienne valeur :

x = 2
x == 5
print(x)
2

Python ne signale rien : une expression seule sur une ligne est permise, elle est simplement calculée pour rien. La correction remplace la comparaison par une affectation ; on en profite pour montrer == à sa vraie place, dans une valeur qu'on affiche :

x = 2
x = 5
print(x)
print(x == 5)
5
True

Et dans l'autre sens ? Écrire if x = 5: pour tester l'égalité est refusé par Python avant toute exécution : il signale une erreur de syntaxe (SyntaxError), et aucune ligne du programme n'est exécutée, pas même celles qui précèdent le if. Un test attend une expression qui vaut vrai ou faux ; l'instruction d'affectation = n'en est pas une. (Les versions récentes de Python suggèrent aussi := dans leur message : cette « affectation-expression » serait acceptée, mais elle range 5 dans x au lieu de le comparer, et le test est toujours vrai — ce n'est pas la correction.) Le test correct s'écrit if x == 5:.

Vérification. La dernière ligne affichée, True, confirme que la valeur rangée est bien 55 : c'est == qui fait le contrôle, sans rien changer à x.

Une affectation copie une valeur, elle ne crée pas de lien

À la ligne 2, b = a range dans b la valeur que a possède à ce moment-là, soit 44. À la ligne 3, on range 77 dans a. Aucune instruction n'a demandé de modifier b : il vaut toujours 44.

étape a b
avant — —
après la ligne 1 4 —
après la ligne 2 4 4
après la ligne 3 7 4
a = 4
b = a
a = 7
print(a, b)
print(b == 4)
7 4
True

En mathématiques, « posons b=ab=a » installe une égalité qui dure tout le raisonnement. En Python, b = a est une photographie : elle recopie une valeur à un instant donné, et la suite du programme peut faire évoluer a et b indépendamment.

⚠️ Réaffecter a ne modifie jamais b, quel que soit le type de la valeur. Les listes réservent une autre surprise, étudiée en E6 : quand deux noms désignent la même liste, modifier cette liste en place (a[0] = 7) se voit par les deux noms. Ce n'est pas une réaffectation, et c'est un autre mécanisme.

Vérification. La ligne True confirme que b a gardé la valeur 44 après la réaffectation de a.

Rappel de cours

Affectation. nom = expression : on évalue l'expression avec les valeurs actuelles, puis on range le résultat sous le nom, qui est créé s'il n'existait pas et écrasé sinon. Toujours la droite d'abord, la gauche ensuite.

Comparaisons. a == b (égal), a != b (différent), <, <=, >, >= : elles ne modifient rien et produisent un booléen, True ou False.

Trace. Un tableau avec une colonne par variable et une ligne par instruction exécutée ; on n'y écrit que les valeurs réellement calculées, et on le contrôle en insérant des print.

Affichage. print(a, b) affiche les valeurs séparées par une espace, puis passe à la ligne.

L'erreur classique

⚠️ Utiliser une variable avant de l'avoir affectée. Python exécute les lignes dans l'ordre : une variable n'existe qu'à partir de la ligne qui l'affecte. Ici le programme s'arrête dès la première ligne, sans rien afficher :

y = x + 1
x = 3
print(y)
NameError: name 'x' is not defined

Il suffit d'échanger les deux premières lignes :

x = 3
y = x + 1
print(y)
4

⚠️ Écrire l'affectation à l'envers. 3 = x est refusé : à gauche du =, il faut un nom, pas un nombre ni un calcul. Le sens de lecture est « x reçoit 33 », jamais « 33 reçoit x ».

⚠️ Lire = comme une égalité qui dure. Après b = a, rien ne relie b à a ; toute trace qui fait « suivre » une variable par une autre est fausse.

Réponse. Le premier programme affiche 16 10 (trace : x vaut successivement 33, 33, 1515, 1515, 1616 ; y, créé à la ligne 2, vaut 55, 55, 1010, 1010) ; x = x + 1 calcule puis range, d'où 121 pour le deuxième ; x == 5 compare sans modifier, le programme de l'élève affiche 2 et se corrige en x = 5 ; if x = 5: est une erreur de syntaxe, le test s'écrit if x == 5: ; b = a copie la valeur du moment, d'où l'affichage 7 4.
Faire cet exercice dans l'app →

Échanger deux variables : le piège, la variable temporaire, l'affectation multiple

DémonstrationDifficulté 2/5

1. Pour échanger les contenus de a et de b, un élève écrit le programme ci-dessous. Qu'affiche-t-il ? Dresser sa trace et expliquer l'échec.

a = 3
b = 8
a = b
b = a
print(a, b)

2. Corriger ce programme à l'aide d'une troisième variable t, en trois affectations, et contrôler par une trace.

3. Python autorise l'écriture a, b = b, a. Prédire l'affichage du programme ci-dessous et expliquer pourquoi, cette fois, aucune valeur n'est perdue.

a = 3
b = 8
a, b = b, a
print(a, b)

4. On peut aussi échanger sans variable auxiliaire : a = a + b, puis b = a - b, puis a = a - b. Démontrer que cette méthode échange deux nombres réels, puis prédire ce qu'elle donne en Python pour a = 0.1 et b = 0.2.

Indices (3)

Après a = b, que reste-t-il de l'ancienne valeur de a ? Une affectation écrase.

Avant d'écraser a, mets sa valeur à l'abri dans t ; c'est t qui la rendra à b.

Pour la méthode arithmétique, suis les valeurs avec deux lettres fixes : pars de a=αa=\alpha et b=βb=\beta.

Correction détaillée
Ce qu'il faut voir

Une affectation écrase : dès que a = b est exécutée, l'ancienne valeur de a est perdue, et aucune instruction ultérieure ne peut la retrouver. Échanger deux variables est donc un problème de sauvegarde : avant d'écraser une valeur dont on aura encore besoin, il faut la mettre à l'abri.

Trois solutions, trois idées différentes :

  • sauvegarder dans une troisième variable, t ;
  • lire les deux valeurs avant de ranger quoi que ce soit, ce que fait a, b = b, a ;
  • coder l'information dans une somme — méthode exacte sur les entiers, et fausse sur les flottants.

👉 Devant toute suite d'affectations, pose-toi la question : quelle valeur cette ligne écrase-t-elle, et en aura-t-on encore besoin ?

Pourquoi l'échange naïf échoue
étape a b
avant — —
après la ligne 1 3 —
après la ligne 2 3 8
après la ligne 3 8 8
après la ligne 4 8 8

La ligne 3 range 88 dans a : le 33 disparaît. La ligne 4 recopie dans b la valeur de a, qui est déjà 88 : elle ne change rien. Le programme affiche deux fois la même valeur :

a = 3
b = 8
a = b
b = a
print(a, b)
8 8

Vérification. Inverser l'ordre des deux dernières affectations ne répare rien : c'est alors le 88 qui est perdu.

a = 3
b = 8
b = a
a = b
print(a, b)
3 3

Les deux ordres échouent, symétriquement : avec deux affectations seulement, l'une des deux valeurs est forcément écrasée avant d'avoir été lue.

La variable temporaire

On sauvegarde la valeur de a dans t avant de l'écraser, puis t la rend à b :

a = 3
b = 8
t = a
a = b
b = t
print(a, b)
8 3
étape a b t
avant — — —
après la ligne 1 3 — —
après la ligne 2 3 8 —
après la ligne 3 3 8 3
après la ligne 4 8 8 3
après la ligne 5 8 3 3

À la ligne 4, a est bien écrasé — mais sa valeur survit dans t. C'est tout le secret.

Vérification. La méthode ne calcule rien, elle déplace des valeurs : elle marche donc pour n'importe quel type, y compris des chaînes de caractères, et même pour deux valeurs de types différents.

a = 'pile'
b = 2.5
t = a
a = b
b = t
print(a, b)
2.5 pile
L'affectation multiple a, b = b, a

Une affectation multiple s'exécute, elle aussi, droite d'abord : Python évalue tout le membre de droite, b, a, ce qui donne le couple (8,3)(8,3) ; ensuite seulement il range 88 dans a et 33 dans b. Au moment où l'on range, les deux anciennes valeurs ont déjà été lues : rien ne peut être perdu. Le couple joue le rôle de la variable t.

a = 3
b = 8
a, b = b, a
print(a, b)
8 3

Il ne faut donc pas lire a, b = b, a comme « a = b puis b = a » : ce serait exactement l'échange naïf de la question 1. Beaucoup de langages n'ont pas cette écriture ; la variable temporaire reste la méthode universelle, celle qu'on écrit en pseudo-code.

Vérification. Un échange appliqué deux fois doit ramener au point de départ :

a = 3
b = 8
a, b = b, a
a, b = b, a
print(a, b)
3 8
La méthode arithmétique : exacte sur les entiers

Notons α\alpha et β\beta les valeurs initiales de a et b, et suivons-les ligne par ligne.

  • Après a = a + b : a vaut α+β\alpha+\beta, b vaut β\beta.
  • Après b = a - b : b vaut (α+β)−β=α(\alpha+\beta)-\beta=\alpha.
  • Après a = a - b : a vaut (α+β)−α=β(\alpha+\beta)-\alpha=\beta.

Les valeurs sont échangées, pour tous réels α\alpha et β\beta. ■\blacksquare L'information « perdue » à la première ligne ne l'est pas vraiment : elle est codée dans la somme.

En Python, les entiers sont exacts et n'ont pas de taille maximale : la démonstration s'y applique telle quelle, même pour de très grands nombres.

a = 10 ** 30
b = 7
a = a + b
b = a - b
a = a - b
print(a, b)
7 1000000000000000000000000000000

Vérification. Avec les valeurs de la question 1, on retrouve bien l'échange :

a = 3
b = 8
a = a + b
b = a - b
a = a - b
print(a, b)
8 3
La même méthode sur des flottants

La démonstration utilise (α+β)−β=α(\alpha+\beta)-\beta=\alpha, vrai pour les réels. Mais 0.1 + 0.2 n'est pas calculé exactement (voir A4) : c'est le flottant 0.30000000000000004, et l'erreur d'arrondi de la somme ne s'annule pas quand on retranche.

a = 0.1
b = 0.2
a = a + b
b = a - b
a = a - b
print(a, b)
print(b == 0.1)
0.2 0.10000000000000003
False

a a bien reçu 0.2, mais b n'a pas récupéré 0.1 : l'échange est faux. Sur des nombres d'ordres de grandeur très différents, c'est pire. Au voisinage de 101610^{16}, deux flottants consécutifs sont espacés de 22 : ajouter 11 ne change rien, et la petite valeur est entièrement perdue.

a = 1e16
b = 1.0
print(a + b == a)
a = a + b
b = a - b
a = a - b
print(a, b)
True
0.0 1e+16

Vérification. La première ligne affichée, True, montre la cause : a + b est égal à a, le 11 a disparu dès la première affectation. Conclusion : la méthode arithmétique est un bel exercice de raisonnement, pas une méthode à employer. On échange avec t ou avec a, b = b, a.

Rappel de cours

Écrasement. Une affectation remplace l'ancienne valeur, qui n'est plus accessible ensuite.

Échange par variable temporaire : t = a, puis a = b, puis b = t. Trois affectations, valables pour toutes les valeurs.

Affectation multiple : a, b = e1, e2 évalue e1 et e2 avec les valeurs actuelles, puis range les résultats. D'où l'échange a, b = b, a.

Entiers et flottants. Les entiers de Python sont exacts, sans taille maximale. Les flottants sont des approximations à 5353 chiffres binaires significatifs : une identité vraie sur les réels peut devenir fausse sur les flottants.

L'erreur classique

⚠️ Échanger par deux affectations. a = b puis b = a produit deux copies de la même valeur, quel que soit l'ordre.

⚠️ Tester un échange sur deux valeurs égales. Avec a et b valant tous deux 55, l'échange naïf « marche » :

a = 5
b = 5
a = b
b = a
print(a, b)
5 5

Le test passe et le programme est faux : un cas test où les deux valeurs sont égales ne peut pas le démasquer. Pour tester un échange, prends deux valeurs différentes, et regarde les deux.

⚠️ Croire qu'une démonstration sur les réels vaut pour les flottants. La méthode arithmétique est juste sur R\mathbb{R} et sur les entiers de Python, fausse sur les flottants.

Réponse. L'échange naïf affiche 8 8 : la ligne a = b écrase le 33 avant qu'on l'ait lu. Avec t = a, a = b, b = t, on obtient 8 3. a, b = b, a évalue tout le membre de droite avant de ranger et affiche 8 3. La méthode arithmétique échange tous réels α\alpha, β\beta et marche sur les entiers de Python, mais pour 0.1 et 0.2 elle affiche 0.2 0.10000000000000003 : l'arrondi de la somme ne s'annule pas.
Faire cet exercice dans l'app →

Division euclidienne en Python : quotient, reste, division décimale et négatifs

DémonstrationDifficulté 2/5

1. Prédire l'affichage du programme ci-dessous.

print(17 // 5, 17 % 5, 17 / 5)
print(-7 // 2, -7 % 2)
print(7 // -2, 7 % -2)

2. Vérifier, pour chacun des trois couples (a,b)(a,b) du programme, l'identité a == b * (a // b) + a % b. Quand b>0b>0, l'opération a // b arrondit-elle a/ba/b vers zéro, ou vers le bas ?

3. Écrire un programme qui affiche le chiffre des centaines, celui des dizaines et celui des unités de n = 4827, sans convertir n en chaîne de caractères.

4. Écrire un programme qui convertit une durée de s = 10000 secondes en heures, minutes et secondes.

Indices (3)

// donne le quotient entier, % le reste, / un flottant. Pour un dividende négatif et b>0b>0, cherche le reste rr tel que 0≤r<b0\leq r<b ; quand le diviseur est négatif, pars de l'identité a == b * (a // b) + a % b.

Le chiffre des unités est le reste de la division par 1010 ; diviser par 1010 avec // efface ce chiffre.

Une heure fait 36003600 secondes : commence par le quotient de s par 36003600, puis travaille sur le reste.

Correction détaillée
Ce qu'il faut voir

Python a trois divisions, et les confondre est la première source d'erreurs de calcul.

  • a / b est la division décimale : elle renvoie toujours un flottant, même quand la division tombe juste.
  • a // b est le quotient entier.
  • a % b est le reste.

Les deux dernières sont liées par l'identité de la division euclidienne, a == b * (a // b) + a % b, que Python garantit pour tous les entiers aa et b≠0b\neq0. Quand b>0b>0, le reste vérifie de plus 0≤r<b0\leq r<b : c'est exactement la division euclidienne du cours, prolongée aux entiers négatifs.

👉 La conséquence à retenir : quand b>0b>0, a // b arrondit a/ba/b vers le bas (vers −∞-\infty), et non vers zéro. Sur les nombres positifs, tu ne verras aucune différence ; sur les négatifs, si.

Trois divisions, trois résultats
print(17 // 5, 17 % 5, 17 / 5)
print(-7 // 2, -7 % 2)
print(7 // -2, 7 % -2)
3 2 3.4
-4 1
-4 -1

Ligne 1. 17=5×3+217=5\times3+2 avec 0≤2<50\leq2<5 : quotient 33, reste 22. La division décimale donne le flottant 3.4.

Ligne 2. −7/2=−3,5-7/2=-3{,}5. Python choisit le quotient −4-4 et le reste 11, car −7=2×(−4)+1-7=2\times(-4)+1 avec 0≤1<20\leq1<2. Le couple (−3,−1)(-3,-1) vérifierait aussi −7=2q+r-7=2q+r, mais avec un reste négatif : ce n'est pas la division euclidienne.

Ligne 3. Le diviseur est négatif. Python garde l'identité et donne au reste le signe du diviseur : 7=(−2)×(−4)+(−1)7=(-2)\times(-4)+(-1).

Vérification. Le cas où la division tombe juste ne doit laisser aucun reste, et / doit malgré tout rendre un flottant :

print(-8 // 2, -8 % 2, 6 / 3)
-4 0 2.0
L'identité de la division euclidienne

On la contrôle par des assert : une instruction assert condition ne fait rien si la condition est vraie, et arrête le programme sinon.

assert 17 == 5 * (17 // 5) + 17 % 5
assert -7 == 2 * (-7 // 2) + -7 % 2
assert 7 == -2 * (7 // -2) + 7 % -2
print('identité vérifiée')
identité vérifiée

Le programme atteint sa dernière ligne : les trois identités sont vraies.

Pourquoi c'est un arrondi vers le bas. Soit b>0b>0, qq = a // b et rr = a % b, avec a=bq+ra=bq+r et 0≤r<b0\leq r<b. En divisant par b>0b>0 : ab=q+rb\frac ab=q+\frac rb avec 0≤rb<10\leq\frac rb<1, donc q≤ab<q+1q\leq\frac ab<q+1, c'est-à-dire

q=⌊ab⌋.q=\left\lfloor\frac ab\right\rfloor .
Pour a=−7a=-7 et b=2b=2 : ⌊−3,5⌋=−4\lfloor-3{,}5\rfloor=-4. Arrondir vers zéro, c'est tronquer ; c'est ce que fait int appliqué à un flottant :

print(-7 // 2, int(-7 / 2))
-4 -3

Vérification. Sur un couple de positifs, les deux arrondis coïncident, et sur un couple négatif le quotient et le reste redonnent bien aa :

a = -7
b = 2
q = a // b
r = a % b
print(q, r, b * q + r)
print(0 <= r < b)
print(17 // 5 == int(17 / 5))
-4 1 -7
True
True
Les chiffres d'un entier

Pour n≥0n\geq0, n=10×(n // 10)+(n % 10)n=10\times(n\ //\ 10)+(n\ \%\ 10) avec un reste entre 00 et 99 : le reste par 1010 est le chiffre des unités, et le quotient par 1010 est nn privé de ce chiffre. On recommence sur le quotient pour obtenir les dizaines, et ainsi de suite.

n = 4827
u = n % 10
d = (n // 10) % 10
c = (n // 100) % 10
m = n // 1000
print(m, c, d, u)
print(1000*m + 100*c + 10*d + u)
4 8 2 7
4827

Pour 48274827 : le chiffre des centaines est 88, celui des dizaines 22, celui des unités 77. Par exemple n // 100 vaut 4848, dont le reste par 1010 est 88.

Vérification. La deuxième ligne affichée reconstruit nn à partir de ses chiffres : on retrouve bien 48274827. Ce découpage par % et // est l'algorithme qu'on répétera dans une boucle au lot B, pour traiter un entier de longueur quelconque.

Convertir des secondes

On retire d'abord les heures entières, puis on découpe le reste en minutes et secondes.

s = 10000
h = s // 3600
reste = s % 3600
m = reste // 60
sec = reste % 60
print(h, m, sec)
print(3600 * h + 60 * m + sec)
2 46 40
10000

10000=3600×2+280010000=3600\times2+2800, puis 2800=60×46+402800=60\times46+40 : la durée vaut 2 h 46 min 40 s.

Vérification. La dernière ligne recalcule le nombre total de secondes à partir du résultat et retrouve 1000010000. Les restes ont aussi la bonne taille : 46<6046<60 et 40<6040<60, comme il se doit pour des minutes et des secondes.

Rappel de cours

Division euclidienne. Pour a∈Za\in\mathbb{Z} et b∈N∗b\in\mathbb{N}^*, il existe un unique couple d'entiers (q,r)(q,r) tel que a=bq+ra=bq+r et 0≤r<b0\leq r<b ; alors q=⌊a/b⌋q=\lfloor a/b\rfloor. En Python, a // b vaut qq et a % b vaut rr. (Voir le chapitre d'arithmétique.)

Diviseur négatif. Python garde l'identité a == b * (a // b) + a % b et donne au reste le signe du diviseur : 7 % -2 vaut -1.

Division décimale. a / b renvoie un flottant, même quand bb divise aa.

Chiffres. Pour n≥0n\geq0, n % 10 est le chiffre des unités de nn, et n // 10 efface ce chiffre.

L'erreur classique

⚠️ Croire que // tronque vers zéro. -7 // 2 vaut -4, pas -3 ; la troncature, c'est int(-7 / 2). D'autres langages, comme C ou Java, tronquent vers zéro : un algorithme recopié d'un autre langage peut changer de résultat sur les nombres négatifs.

⚠️ Utiliser / là où il faut //. Le résultat de / est un flottant ; or un indice de liste, un nombre de tours de boucle ou un nombre d'objets doit être un entier. Et sur de grands nombres, passer par un flottant fait perdre des chiffres :

n = 10 ** 17
print(n // 3)
print(int(n / 3))
33333333333333333
33333333333333332

La première ligne est exacte ; la seconde est fausse au dernier chiffre, parce que n / 3 a été arrondi à un flottant, qui ne garde que 5353 chiffres binaires. Pour une division entière, on écrit //, jamais int(… / …).

⚠️ Diviser par zéro. a // 0 et a % 0 arrêtent le programme sur une ZeroDivisionError, comme a / 0.

Réponse. Le programme affiche 3 2 3.4, puis -4 1, puis -4 -1. L'identité a == b * (a // b) + a % b tient pour les trois couples, et quand b>0b>0, a // b est ⌊a/b⌋\lfloor a/b\rfloor : un arrondi vers le bas, pas vers zéro (-7 // 2 vaut -4, int(-7 / 2) vaut -3). Les chiffres de 48274827 : centaines 88, dizaines 22, unités 77. Enfin 1000010000 s font 2 h 46 min 40 s.
Faire cet exercice dans l'app →

Les flottants ne sont pas des réels : 0,1 + 0,2, écriture binaire et tolérance

DémonstrationDifficulté 3/5

1. Prédire, puis constater en lançant le programme.

print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(0.5 + 0.25 == 0.75)

2. Démontrer que le nombre 110\frac{1}{10} ne peut pas s'écrire avec un nombre fini de chiffres en base 22. Pourquoi, en revanche, le calcul 0,5+0,25=0,750{,}5+0{,}25=0{,}75 est-il exact ?

3. Proposer un test d'égalité « à une tolérance près » entre 0.1 + 0.2 et 0.3, et l'écrire en Python.

4. Prédire ce qu'affiche le programme suivant, qui ajoute dix fois 0,10{,}1 à zéro.

s = 0
for k in range(10):
    s = s + 0.1
print(s)
print(s == 1)
Indices (3)

Un nombre à écriture binaire finie s'écrit m2k\frac{m}{2^k} avec mm et kk entiers. Peut-on écrire 110\frac1{10} ainsi ?

0,50{,}5, 0,250{,}25 et 0,750{,}75 sont des fractions dont le dénominateur est une puissance de 22.

Deux flottants sont « égaux à une tolérance près » quand la valeur absolue de leur différence est petite : abs(x - y) < eps.

Correction détaillée
Ce qu'il faut voir

Un flottant est un nombre à virgule écrit en base 22, avec un nombre fixe de chiffres. Python range un nombre comme 0.1 sous la forme m×2em\times2^e, où mm est un entier d'au plus 5353 chiffres binaires (norme IEEE 754, dite « double précision »). Tout réel qui ne s'écrit pas exactement ainsi est arrondi au flottant le plus proche — et c'est le cas de 0,10{,}1, de 0,20{,}2 et de 0,30{,}3. Chaque opération arrondit ensuite son résultat : les petites erreurs s'ajoutent, et parfois se compensent.

Python n'est pas en cause : la plupart des langages utilisent ces mêmes flottants et donnent les mêmes résultats.

👉 La conséquence pratique, à appliquer sans exception : on ne teste jamais l'égalité de deux flottants issus d'un calcul avec == ; on teste que leur écart est petit.

Ce que Python calcule vraiment
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(0.5 + 0.25 == 0.75)
0.30000000000000004
False
True

0.1 et 0.2 sont déjà des approximations ; leur somme, arrondie à son tour, tombe sur le flottant voisin de celui qui approche le mieux 0,30{,}3. Les deux flottants comparés par == sont différents : le test rend False. print affiche le plus court nombre décimal qui désigne sans ambiguïté un flottant ; c'est pourquoi 0.1 s'affiche 0.1, alors que la valeur rangée n'est pas exactement 110\frac1{10}, et pourquoi la somme s'affiche avec tous ses chiffres.

Vérification. L'écart entre les deux flottants est minuscule, et c'est exactement 2−542^{-54}, l'espacement des flottants entre 0,250{,}25 et 0,50{,}5 : ils sont consécutifs.

print(0.1 + 0.2 - 0.3)
print(0.1 + 0.2 - 0.3 == 2 ** -54)
5.551115123125783e-17
True
Pourquoi 1/10 n'a pas d'écriture binaire finie

Une écriture binaire finie 0,b1b2…bk0{,}b_1b_2\dots b_k représente le nombre m2k\frac{m}{2^k}, où mm est l'entier qui s'écrit b1b2…bkb_1b_2\dots b_k en base 22. Supposons 110=m2k\frac1{10}=\frac{m}{2^k} avec mm et kk entiers. Alors 2k=10m=2×5×m2^k=10m=2\times5\times m, donc 55 divise 2k=2×2×⋯×22^k=2\times2\times\dots\times2. C'est impossible : un nombre premier qui divise un produit divise l'un des facteurs (lemme d'Euclide, conséquence du théorème de Gauss, voir le chapitre d'arithmétique), et 55 ne divise pas 22. ■\blacksquare

C'est le même phénomène que 13=0,333…\frac13=0{,}333\dots en base 1010 : l'écriture existe, mais elle ne s'arrête jamais. Le même raisonnement, appuyé sur le théorème de Gauss, donne le critère général : un rationnel irréductible pq\frac pq a une écriture binaire finie si et seulement si qq est une puissance de 22. 0,5=120{,}5=\frac12, 0,25=140{,}25=\frac14 et 0,75=340{,}75=\frac34 sont dans ce cas : ils sont rangés exactement, leur somme aussi, et 0.5 + 0.25 == 0.75 vaut True. 0,1=1100{,}1=\frac1{10}, 0,2=150{,}2=\frac15 et 0,3=3100{,}3=\frac3{10} ne le sont pas.

Vérification. On peut calculer les chiffres binaires de 110\frac1{10} avec des entiers, donc sans aucune erreur d'arrondi : on double le numérateur, le chiffre est le quotient par 1010, et on garde le reste.

p = 1
q = 10
for k in range(12):
    p = 2 * p
    print(p // q, end='')
    p = p % q
print()
000110011001

On lit 110=0,000110011001…\frac1{10}=0{,}000110011001\dots en base 22 : après le premier chiffre, le motif 00110011 se répète. Les restes successifs valent 2,4,8,62, 4, 8, 6, puis de nouveau 22 : le calcul repasse par le même état, donc les chiffres se répètent indéfiniment, comme le prévoit la démonstration.

Comparer à une tolérance près

On déclare deux flottants égaux quand leur écart est plus petit qu'une tolérance eps choisie à l'avance :

x = 0.1 + 0.2
y = 0.3
eps = 1e-9
print(abs(x - y) < eps)
print(abs(0.3 - 0.31) < eps)
True
False

eps doit être bien plus grand que les erreurs d'arrondi (ici de l'ordre de 10−1610^{-16}) et bien plus petit que les écarts qui ont un sens pour le problème. La deuxième ligne sert de contre-témoin : deux nombres réellement différents ne sont pas déclarés égaux.

Une tolérance absolue comme 10−910^{-9} n'a de sens que pour des nombres de l'ordre de l'unité. Pour des nombres de tailles quelconques, on compare l'écart à la taille des nombres : c'est ce que fait math.isclose, dont la tolérance relative vaut 10−910^{-9} par défaut.

import math
print(math.isclose(0.1 + 0.2, 0.3))
d = 0.1 + 0.2 - 0.3
print(math.isclose(d, 0))
True
False

Vérification. La dernière ligne montre la limite d'une tolérance purement relative : comparé à 00, un nombre non nul n'est jamais « proche », puisque l'écart admis est proportionnel à la taille des nombres, ici nulle. Pour tester qu'un résultat est presque nul, on revient à abs(d) < eps.

Dix fois 0,1 ne font pas 1
s = 0
for k in range(10):
    s = s + 0.1
print(s)
print(s == 1)
0.9999999999999999
False

(La boucle for répète dix fois l'instruction décalée ; elle est détaillée au lot B.) Chaque addition arrondit son résultat, et les erreurs s'accumulent : après dix additions, la somme est juste en dessous de 11.

Vérification. On fait afficher la somme après chaque addition :

s = 0
for k in range(10):
    s = s + 0.1
    print(k + 1, s)
1 0.1
2 0.2
3 0.30000000000000004
4 0.4
5 0.5
6 0.6
7 0.7
8 0.7999999999999999
9 0.8999999999999999
10 0.9999999999999999

L'écart apparaît dès la troisième addition. À la quatrième, les arrondis se compensent et la somme retombe sur le flottant le plus proche de 0,40{,}4 — qui n'est pas exactement 0,40{,}4, mais s'affiche 0.4. À partir de la huitième, la somme s'écarte de nouveau et ne revient plus. Quand la grandeur est décimale par nature (des centimes, des dixièmes), le remède est de compter en entiers : dix fois 11 dixième font exactement 1010 dixièmes, et 10 / 10 vaut 1.0.

Rappel de cours

Flottant. Un float Python est un nombre de la forme m×2em\times2^e avec mm entier d'au plus 5353 chiffres binaires ; les autres réels sont arrondis au plus proche, et chaque opération arrondit son résultat.

Rationnels représentables. Un rationnel irréductible pq\frac pq a une écriture binaire finie si et seulement si qq est une puissance de 22 : 0,50{,}5, 0,250{,}25, 0,750{,}75 oui ; 0,10{,}1, 0,20{,}2, 0,30{,}3 non.

Comparer. abs(x - y) < eps (tolérance absolue) ou math.isclose(x, y) (tolérance relative, 10−910^{-9} par défaut). Jamais == entre flottants calculés.

Affichage. print affiche le plus court nombre décimal qui désigne le flottant sans ambiguïté : 0.1 s'affiche 0.1, bien que la valeur rangée ne soit pas 110\frac1{10}.

L'erreur classique

⚠️ Arrêter une boucle sur une égalité de flottants. La boucle ci-dessous devait s'arrêter quand x atteint 11 ; or x passe de 0.9999999999999999 à 1.0999999999999999 sans jamais valoir exactement 11, et le programme ne termine pas :

x = 0
while x != 1:
    x = x + 0.1

Remplacer != par < ne suffit pas : la boucle termine, mais elle fait un tour de trop.

x = 0
n = 0
while x < 1:
    x = x + 0.1
    n = n + 1
print(n, x)
11 1.0999999999999999

Après dix tours, x vaut 0.9999999999999999, qui est encore strictement inférieur à 11 : la boucle repart pour un onzième tour. Quand on sait combien de tours faire, on compte avec un entier — for k in range(10) — et le flottant n'intervient plus dans la décision d'arrêt.

⚠️ Choisir une tolérance sans regarder l'ordre de grandeur. 10−910^{-9} est énorme pour des nombres de l'ordre de 10−1210^{-12} et dérisoire pour des nombres de l'ordre de 101210^{12}.

Réponse. 0.1 + 0.2 s'affiche 0.30000000000000004, et 0.1 + 0.2 == 0.3 vaut False, alors que 0.5 + 0.25 == 0.75 vaut True. Écrire 110=m2k\frac1{10}=\frac m{2^k} imposerait que 55 divise 2k2^k : 0,10{,}1 n'a pas d'écriture binaire finie, et seuls les rationnels pq\frac pq irréductibles avec qq puissance de 22 en ont une. On compare avec abs(x - y) < 1e-9 ou math.isclose(x, y). Dix ajouts de 0,10{,}1 donnent 0.9999999999999999, et s == 1 vaut False.
Faire cet exercice dans l'app →

Priorités des opérateurs : traduire une formule sans la trahir

DémonstrationDifficulté 2/5

1. Prédire l'affichage du programme ci-dessous, ligne par ligne.

print(2 + 3 * 4)
print(-2 ** 2)
print(2 ** 3 ** 2)
print(7 - 3 - 2)
print(12 / 2 * 3)

2. Pour calculer la moyenne de a = 8 et b = 14, un élève écrit a + b / 2. Qu'obtient-il ? Corriger.

3. On cherche les racines du trinôme 2x2−3x−52x^2-3x-5. Un élève écrit le programme ci-dessous. Qu'affiche-t-il ? Placer les parenthèses qui manquent, puis vérifier les deux racines en les réinjectant dans le trinôme.

import math
a = 2
b = -3
c = -5
d = b ** 2 - 4 * a * c
x1 = -b + math.sqrt(d) / 2 * a
print(d, x1)

4. Que valent 9 ** 1 / 2 et 9 ** (1 / 2) ? Laquelle des deux calcule 9\sqrt9 ?

Indices (3)

Du plus prioritaire au moins prioritaire : parenthèses, puissance **, signe moins, puis * et /, puis + et -.

** se regroupe de droite à gauche ; - et / se regroupent de gauche à droite.

Une barre de fraction groupe tout son numérateur et tout son dénominateur : sur une ligne, il faut des parenthèses pour chacun.

Correction détaillée
Ce qu'il faut voir

Une expression Python est une formule écrite sur une seule ligne : plus de barre de fraction, plus d'exposant surélevé. C'est l'ordre des priorités qui décide alors de la structure. Il prolonge celui des mathématiques : les parenthèses d'abord, puis la puissance **, puis le signe moins placé devant un nombre, puis *, /, //, %, puis + et -. À priorité égale, on regroupe de gauche à droite — sauf pour **, qui se regroupe de droite à gauche.

Deux conséquences surprennent : la puissance passe avant le signe moins écrit devant elle, et 2 ** 3 ** 2 se lit 2(32)2^{(3^2)}. Ces deux règles sont d'ailleurs celles des mathématiques : −22=−4-2^2=-4 et 232=292^{3^2}=2^9.

👉 La règle pratique : dès qu'une barre de fraction ou un exposant disparaît en passant sur une ligne, tu remets les parenthèses que la barre rendait inutiles.

Cinq lignes, cinq règles
print(2 + 3 * 4)
print(-2 ** 2)
print(2 ** 3 ** 2)
print(7 - 3 - 2)
print(12 / 2 * 3)
14
-4
512
2
18.0
expression lecture de Python valeur
2 + 3 * 4 2+(3×4)2+(3\times4) 14
-2 ** 2 −(22)-(2^2) -4
2 ** 3 ** 2 2(32)=292^{(3^2)}=2^9 512
7 - 3 - 2 (7−3)−2(7-3)-2 2
12 / 2 * 3 (12/2)×3(12/2)\times3 18.0

La dernière ligne rend un flottant, 18.0, parce que / rend toujours un flottant (voir A3).

Vérification. Si les priorités ont été bien lues, placer les parenthèses autrement doit changer chaque valeur :

print((-2) ** 2)
print((2 ** 3) ** 2)
print(7 - (3 - 2))
print(12 / (2 * 3))
4
64
6
2.0

Les quatre valeurs changent : les parenthèses ne sont pas décoratives.

La moyenne et la barre de fraction

a + b / 2 se lit a+b2=8+7a+\frac b2=8+7 : la division passe avant l'addition, et seul b est divisé.

a = 8
b = 14
print(a + b / 2)
15.0

La moyenne a+b2\frac{a+b}{2} a un numérateur entier ; sur une ligne, il faut le parenthéser :

a = 8
b = 14
m = (a + b) / 2
print(m)
print(min(a, b) <= m <= max(a, b))
11.0
True

Vérification. Une moyenne est toujours comprise entre la plus petite et la plus grande valeur : la dernière ligne le confirme pour 1111. Le résultat fautif, 1515, dépasse 1414 : ce test de vraisemblance, d'une ligne, suffisait à le démasquer.

Le discriminant et les racines

Δ=b2−4ac=9+40=49\Delta=b^2-4ac=9+40=49 : cette ligne de l'élève est juste. La suivante se lit −b+(Δ2)×a-b+\left(\frac{\sqrt\Delta}{2}\right)\times a : on divise par 22 puis on multiplie par aa, et −b-b n'est pas divisé du tout. Deux erreurs de parenthèses, et un résultat de 3+3,5×2=103+3{,}5\times2=10 :

import math
a = 2
b = -3
c = -5
d = b ** 2 - 4 * a * c
x1 = -b + math.sqrt(d) / 2 * a
print(d, x1)
49 10.0

La formule −b±Δ2a\dfrac{-b\pm\sqrt\Delta}{2a} a un numérateur et un dénominateur à parenthéser :

import math
a = 2
b = -3
c = -5
d = b ** 2 - 4 * a * c
r = math.sqrt(d)
x1 = (-b + r) / (2 * a)
x2 = (-b - r) / (2 * a)
print(d, x1, x2)
print(a*x1**2 + b*x1 + c)
print(a*x2**2 + b*x2 + c)
49 2.5 -1.0
0.0
0.0

Vérification. Les deux dernières lignes réinjectent les racines dans le trinôme : 2×2,52−3×2,5−5=12,5−7,5−5=02\times2{,}5^2-3\times2{,}5-5=12{,}5-7{,}5-5=0 et 2+3−5=02+3-5=0. Dans a*x1**2, la puissance passe avant la multiplication : c'est bien a×x12a\times x_1^2.

Une subtilité au passage : b ** 2 vaut 99, parce que la variable b contient −3-3 ; mais écrire directement -3 ** 2 donne −9-9, le signe moins s'appliquant après la puissance :

b = -3
print(b ** 2, -3 ** 2)
9 -9
Racine carrée et puissance fractionnaire
print(9 ** 1 / 2)
print(9 ** (1 / 2))
4.5
3.0

9 ** 1 / 2 se lit 912=4,5\frac{9^1}{2}=4{,}5 : la puissance passe avant la division. 9 ** (1 / 2) calcule 91/2=9=39^{1/2}=\sqrt9=3. Pour une racine carrée, le plus lisible reste math.sqrt, ou x ** 0.5 quand x est positif ou nul.

Vérification. Par deux autres chemins : la racine obtenue, élevée au carré, redonne 99, et math.sqrt donne la même valeur.

import math
r = 9 ** (1 / 2)
print(r ** 2, math.sqrt(9))
9.0 3.0
Rappel de cours

Priorités, de la plus forte à la plus faible : parenthèses ; ** ; signe - placé devant ; *, /, //, % ; +, - ; puis les comparaisons, not, and, or (voir A6).

Regroupement. ** se regroupe de droite à gauche : 2 ** 3 ** 2 vaut 292^9. Tous les autres opérateurs arithmétiques se regroupent de gauche à droite : 7 - 3 - 2 vaut (7−3)−2(7-3)-2.

Traduire une fraction. a+bc+d\frac{a+b}{c+d} s'écrit (a + b) / (c + d) ; −b+Δ2a\frac{-b+\sqrt\Delta}{2a} s'écrit (-b + math.sqrt(d)) / (2 * a).

L'erreur classique

⚠️ Oublier les parenthèses du dénominateur. x / 2 * a divise par 22 puis multiplie par aa : 12 / 2 * 3 vaut 18.0, pas 2.0. Le dénominateur 2a2a s'écrit (2 * a).

⚠️ Confondre -2 ** 2 et (-2) ** 2. Le premier vaut −4-4, le second 44.

⚠️ Écrire ^ pour la puissance, par habitude de la calculatrice ou du tableur. En Python, ^ est une autre opération sur les entiers (le « ou exclusif » bit à bit) : le programme tourne, sans aucun message, et donne un résultat faux.

x = 2
print(x ^ 3)
print(x ** 3)
1
8

La première ligne devait afficher 23=82^3=8 ; elle affiche 11. La puissance s'écrit **.

Réponse. Le programme affiche 14, -4, 512, 2, 18.0 : la multiplication passe avant l'addition, la puissance avant le signe moins, regroupée de droite à gauche ; - et / se regroupent de gauche à droite. a + b / 2 vaut 15.0, la moyenne (a + b) / 2 vaut 11.0. Le discriminant vaut 4949 ; la formule fautive donne 10.0, la bonne, (-b + r) / (2 * a), donne les racines 2,52{,}5 et −1-1, vérifiées par réinjection. 9 ** 1 / 2 vaut 4.5, 9 ** (1 / 2) vaut 3.0.
Faire cet exercice dans l'app →

Booléens : and, or, not, comparaisons enchaînées et évaluation paresseuse

DémonstrationDifficulté 3/5

1. Prédire l'affichage du programme ci-dessous.

x = 0.5
print(0 < x < 1)
print(0 < x and x < 1)
print(not x > 0 or x == 1)
print(x > 1 or x < 0)

2. Quand x vaut 00, laquelle des deux lignes suivantes s'exécute sans erreur, et que vaut alors la variable calculée ? Expliquer.

b1 = x != 0 and 1 / x > 2
b2 = 1 / x > 2 and x != 0

3. Écrire, sans utiliser not, une condition vraie exactement quand x n'appartient pas à l'intervalle [0,1][0,1], et la tester pour x∈{−1 ; 0 ; 0,5 ; 1 ; 2}x\in\{-1\,;\,0\,;\,0{,}5\,;\,1\,;\,2\}. Quel résultat de logique utilise-t-on ?

4. Trois longueurs aa, bb, cc forment un triangle non aplati si et seulement si chacune est strictement inférieure à la somme des deux autres. Écrire la condition correspondante en Python et la tester sur (3,4,5)(3,4,5), (1,2,3)(1,2,3) et (1,1,5)(1,1,5).

Indices (3)

Les comparaisons se calculent avant not, not avant and, and avant or.

A and B ne calcule pas B quand A est faux : le résultat est déjà connu.

La négation d'un « et » est un « ou » : c'est l'une des lois de Morgan.

Correction détaillée
Ce qu'il faut voir

Une condition est une expression comme une autre : elle se calcule, et sa valeur est un booléen, True ou False. Les comparaisons (<, ==, !=…) fabriquent des booléens ; and, or et not les combinent, avec les tables de vérité des connecteurs « et », « ou » (inclusif) et « non » de la logique.

Deux traits sont propres à Python. On peut enchaîner les comparaisons comme en mathématiques : 0 < x < 1 signifie 0 < x and x < 1. Et and, or sont paresseux : ils ne calculent leur second membre que si le premier ne suffit pas à conclure.

👉 En logique, « PP et QQ » et « QQ et PP » sont équivalents ; en Python, l'ordre des membres d'un and compte dès que le second peut échouer. Tu places d'abord la condition qui protège l'autre — « x non nul » avant la division par x.

Lire une condition
x = 0.5
print(0 < x < 1)
print(0 < x and x < 1)
print(not x > 0 or x == 1)
print(x > 1 or x < 0)
True
True
False
False

Les deux premières lignes disent la même chose, l'une en notation enchaînée, l'autre avec and : 0<0,5<10<0{,}5<1 est vrai. La troisième se lit, d'après les priorités, (not (x > 0)) or (x == 1) : x>0x>0 est vrai, donc sa négation est fausse, et x=1x=1 est faux ; le « ou » de deux faux est faux. La quatrième dit que xx est hors de [0,1][0,1] : faux pour 0,50{,}5.

Vérification. Pour s'assurer que not s'applique avant or, on choisit une valeur qui sépare les deux lectures possibles de la troisième ligne affichée — avec x=0,5x=0{,}5, elles donnent toutes deux False, donc ce test ne prouverait rien. Avec x=1x=1 :

x = 1
print(not x > 0 or x == 1)
print((not x > 0) or x == 1)
print(not (x > 0 or x == 1))
True
True
False

La ligne sans parenthèses coïncide avec la première lecture, pas avec la seconde : Python applique bien not avant or.

L'évaluation paresseuse

Avec x valant 00, dans x != 0 and 1 / x > 2, le premier membre x != 0 est faux ; un « et » dont un membre est faux est faux, donc Python conclut False sans calculer 1 / x.

x = 0
b1 = x != 0 and 1 / x > 2
print(b1)
False

Dans l'ordre inverse, Python commence par 1 / x et s'arrête sur une division par zéro :

x = 0
b2 = 1 / x > 2 and x != 0
print(b2)
ZeroDivisionError: division by zero

Même principe pour or : dans A or B, si A est vrai, le résultat est vrai et B n'est pas calculé.

x = 0
print(x == 0 or 1 / x > 2)
True

Vérification. Quand x≠0x\neq0, les deux ordres doivent donner la même valeur — seul le cas x=0x=0 les sépare. Avec x=0,25x=0{,}25, on a 1x=4>2\frac1x=4>2 :

x = 0.25
print(x != 0 and 1 / x > 2)
print(1 / x > 2 and x != 0)
True
True
Nier une condition : les lois de Morgan

x∈[0,1]x\in[0,1] s'écrit 0 <= x <= 1, c'est-à-dire 0 <= x and x <= 1. Sa négation, par la loi de Morgan « non (PP et QQ) équivaut à (non PP) ou (non QQ) », est « non (0≤x0\leq x) ou non (x≤1x\leq1) », soit x<0x<0 ou x>1x>1 :

h = x < 0 or x > 1

On la teste contre la condition initiale sur les cinq valeurs demandées ; l'assert arrêterait le programme si les deux conditions n'étaient pas contraires.

for x in [-1, 0, 0.5, 1, 2]:
    d = 0 <= x <= 1
    h = x < 0 or x > 1
    assert h == (not d)
    print(x, d, h)
-1 False True
0 True False
0.5 True False
1 True False
2 False True

Vérification. Les deux colonnes sont contraires sur toutes les lignes, y compris aux bornes 00 et 11 : ces bornes appartiennent à [0,1][0,1], d'où les inégalités strictes dans la négation. Les lois de Morgan sont démontrées au chapitre Raisonnement, par tables de vérité ; ici on les applique, et le programme les contrôle sur des instances.

La condition de triangle

On écrit les trois inégalités strictes, reliées par and. Pour la tester sur trois triplets sans recopier la condition, on la place dans une fonction (les fonctions sont étudiées au lot E) :

def triangle(a, b, c):
    t1 = a < b + c
    t2 = b < a + c
    t3 = c < a + b
    return t1 and t2 and t3

print(triangle(3, 4, 5))
print(triangle(1, 2, 3))
print(triangle(1, 1, 5))
True
False
False

(3,4,5)(3,4,5) vérifie les trois inégalités. (1,2,3)(1,2,3) échoue sur la troisième, 3<1+23<1+2, qui est une égalité : ces trois longueurs donnent un triangle aplati, dont les trois sommets sont alignés. (1,1,5)(1,1,5) échoue aussi sur la troisième : 55 est plus long que 1+11+1.

Vérification. Le cas aplati est le seul qui distingue les inégalités strictes des inégalités larges : avec <=, il serait accepté.

print(3 < 1 + 2, 3 <= 1 + 2)
False True
Rappel de cours

Booléens. True et False ; les comparaisons ==, !=, <, <=, >, >= en produisent.

Connecteurs. A and B est vrai si les deux le sont ; A or B est vrai si l'un au moins l'est ; not A échange vrai et faux. Priorités : les comparaisons, puis not, puis and, puis or.

Évaluation paresseuse. A and B ne calcule pas B si A est faux ; A or B ne calcule pas B si A est vrai.

Comparaisons enchaînées. a < b < c signifie a < b and b < c.

Lois de Morgan. not (A and B) équivaut à not A or not B, et not (A or B) à not A and not B.

L'erreur classique

⚠️ Écrire x == 1 or 2 pour « x vaut 11 ou 22 ». Python lit (x == 1) or 2. Quand x == 1 est faux, or renvoie son second membre, le nombre 22, qu'un if considère comme vrai (tout nombre non nul compte comme vrai). Le test passe donc toujours :

x = 3
print(x == 1 or 2)
print(x == 1 or x == 2)
2
False

La bonne écriture répète la comparaison : x == 1 or x == 2.

⚠️ Nier un « et » en gardant « et ». Pour « xx hors de [0,1][0,1] », la condition x < 0 and x > 1 n'est jamais vraie : aucun nombre n'est à la fois négatif et plus grand que 11.

x = 2
print(x < 0 and x > 1)
False

22 est pourtant hors de [0,1][0,1]. La négation d'un « et » est un « ou ».

Réponse. Le premier programme affiche True, True, False, False : les comparaisons passent avant not, not avant and, and avant or. Avec x valant 00, x != 0 and 1 / x > 2 vaut False sans calculer la division, tandis que l'ordre inverse lève ZeroDivisionError. Par Morgan, la négation de 0 <= x <= 1 est x < 0 or x > 1. La condition de triangle a < b + c and b < a + c and c < a + b est vraie pour (3,4,5)(3,4,5), fausse pour (1,2,3)(1,2,3) (aplati) et pour (1,1,5)(1,1,5).
Faire cet exercice dans l'app →

Année bissextile : l'ordre des tests

DémonstrationDifficulté 2/5

Dans le calendrier grégorien, une année est bissextile si elle est divisible par 44 sans l'être par 100100, ou si elle est divisible par 400400.

1. Écrire une fonction bissextile(a) qui renvoie un booléen à l'aide d'un seul if … elif … else, et donner ses réponses pour 20242024, 20232023, 19001900 et 20002000.

2. Un élève a écrit la fonction ci-dessous. Que renvoie-t-elle pour 19001900 ? pour 20002000 ? Expliquer son erreur.

def bissextile(a):
    if a % 4 == 0:
        return True
    elif a % 100 == 0:
        return False
    elif a % 400 == 0:
        return True
    else:
        return False

3. Proposer quatre années de test qui font passer la bonne fonction par ses quatre branches, et vérifier que ce jeu démasque la fonction de l'élève.

4. Combien d'années bissextiles de 16011601 à 20002000 ? En déduire la durée moyenne d'une année grégorienne.

Indices (3)

Un if … elif … else examine ses conditions dans l'ordre et s'arrête à la première vraie : la condition d'un elif n'est lue que si toutes les précédentes sont fausses.

Toute année divisible par 100100 l'est aussi par 44. Dans la fonction de l'élève, quelle branche reçoit 19001900 ?

Range les tests du plus particulier au plus général : 400400, puis 100100, puis 44. Pour la question 4, un compteur dans une boucle for suffit.

Correction détaillée
Ce qu'il faut voir

Un if … elif … else est une suite de filtres essayés dans l'ordre. Python évalue la première condition ; si elle est vraie, il exécute son bloc et saute tout le reste ; sinon il passe à l'elif suivant. Chaque branche ne reçoit donc que les cas que les branches précédentes ont laissé passer : la condition d'un elif porte, sans l'écrire, « et aucune des conditions précédentes n'était vraie ».

C'est pourquoi l'ordre des tests fait partie de l'algorithme. Quand les conditions s'emboîtent — ici, divisible par 400400 entraîne divisible par 100100, qui entraîne divisible par 44 —, il faut tester du plus particulier au plus général. Dans l'ordre inverse, le test le plus large capture tout, et les branches suivantes ne sont jamais atteintes : ce sont des branches mortes.

👉 Ce qu'on retient : avant d'écrire une cascade de tests, se demander quelle condition en contient une autre, et mettre la plus étroite en premier. Puis vérifier avec un jeu de tests qui passe par chaque branche.

La fonction et ses quatre réponses

On teste 400400 d'abord, puis 100100, puis 44 :

def bissextile(a):
    if a % 400 == 0:
        return True
    elif a % 100 == 0:
        return False
    elif a % 4 == 0:
        return True
    else:
        return False

print(bissextile(2024))
print(bissextile(2023))
print(bissextile(1900))
print(bissextile(2000))

Il affiche :

True
False
False
True

Lecture branche par branche. 20242024 n'est divisible ni par 400400 ni par 100100 : les deux premiers tests échouent, le troisième réussit (2024=4×5062024=4\times506). 20232023 est impair : il tombe dans le else. 1900=19×1001900=19\times100 est arrêté par le deuxième test, puisqu'il n'est pas divisible par 400400 (1900=4×400+3001900=4\times400+300). 2000=5×4002000=5\times400 est pris dès le premier test.

Une autre écriture. La règle est une formule booléenne ; on peut la renvoyer directement, sans if :

def bis2(a):
    b4 = a % 4 == 0
    b100 = a % 100 == 0
    b400 = a % 400 == 0
    return b4 and (not b100 or b400)

for a in [2024, 2023, 1900, 2000]:
    print(a, bis2(a))
2024 True
2023 False
1900 False
2000 True

Mêmes réponses. La cascade de tests garde un avantage pédagogique : chaque branche correspond à une ligne de la règle, et se teste isolément.

Pourquoi la fonction de l'élève se trompe sur 1900

On relance la fonction de l'élève sur les deux années :

def bissextile(a):
    if a % 4 == 0:
        return True
    elif a % 100 == 0:
        return False
    elif a % 400 == 0:
        return True
    else:
        return False

print(bissextile(1900))
print(bissextile(2000))
True
True

19001900 est déclarée bissextile : c'est faux, le 29 février 1900 n'a pas existé. 20002000 est déclarée bissextile : c'est juste, mais par hasard.

Le mécanisme. 19001900 est divisible par 44 (1900=4×4751900=4\times475), donc le premier test est vrai et la fonction renvoie True sans jamais regarder la divisibilité par 100100. Ce n'est pas un accident propre à 19001900 : toute année divisible par 100100 est divisible par 44 (car 100=4×25100=4\times25), donc elle est toujours capturée par le premier test. Le deuxième test n'est atteint que par une année non divisible par 44, qui n'est jamais divisible par 100100 : il est toujours faux. Même chose pour le troisième. Les branches elif a % 100 == 0 et elif a % 400 == 0 sont mortes : on pourrait les effacer sans rien changer, et la fonction se réduit à « divisible par 44 ».

20002000 reçoit la bonne réponse parce que « divisible par 44 » et la vraie règle coïncident sur les multiples de 400400 ; elles ne diffèrent que sur les multiples de 100100 qui ne sont pas multiples de 400400 : 17001700, 18001800, 19001900, 21002100…

Correction. Garder les trois tests mais inverser leur ordre (400400, 100100, 44) : c'est la fonction de la question 1.

Un jeu de tests qui passe par chaque branche

Quatre branches, donc quatre années au moins, chacune choisie pour atterrir dans une branche différente de la bonne fonction :

année divisible par 400 par 100 par 4 branche attendu
2000 oui oui oui 1re True
1900 non oui oui 2e False
2024 non non oui 3e True
2023 non non non else False

On écrit ces attendus sous forme d'assert : assert c ne fait rien si c est vrai, et arrête le programme sinon. Un programme dont tous les assert passent n'affiche rien et se termine normalement — c'est le signe que les tests réussissent.

def bissextile(a):
    if a % 400 == 0:
        return True
    elif a % 100 == 0:
        return False
    elif a % 4 == 0:
        return True
    else:
        return False

assert bissextile(2000)
assert not bissextile(1900)
assert bissextile(2024)
assert not bissextile(2023)

Le même jeu de tests, appliqué à la fonction de l'élève, s'arrête sur l'année 19001900 :

def bissextile(a):
    if a % 4 == 0:
        return True
    elif a % 100 == 0:
        return False
    elif a % 400 == 0:
        return True
    else:
        return False

assert bissextile(2000)
assert not bissextile(1900)
assert bissextile(2024)
assert not bissextile(2023)
AssertionError

Le jeu de tests démasque l'erreur parce qu'il contient une année de la deuxième branche. Un jeu réduit à 20242024, 20232023 et 20002000 aurait reçu trois réponses justes et laissé passer la fonction fausse : tester n'est probant que si chaque branche est exercée.

Quatre siècles de calendrier

On compte avec un compteur dans une boucle for :

def bissextile(a):
    if a % 400 == 0:
        return True
    elif a % 100 == 0:
        return False
    elif a % 4 == 0:
        return True
    else:
        return False

n = 0
for a in range(1601, 2001):
    if bissextile(a):
        n = n + 1
print(n)
97

Recoupement par le calcul. De 16011601 à 20002000 il y a 400400 années, dont 100100 multiples de 44 ; on retire les 44 multiples de 100100 (17001700, 18001800, 19001900, 20002000) et on rend le seul multiple de 400400 (20002000) : 100−4+1=97100-4+1=97. ✓ La fonction de l'élève en compterait 100100 : elle se trompe exactement sur 17001700, 18001800 et 19001900.

Durée moyenne. Quatre siècles comptent 400×365+97=146 097400\times365+97=146\,097 jours, soit une année moyenne de 146 097/400=365,2425146\,097/400=365{,}2425 jours — c'était le but de la réforme grégorienne de 1582 : coller à l'année des saisons. Et 146 097146\,097 est divisible par 77 : quatre siècles font un nombre entier de semaines, si bien que le calendrier grégorien se répète à l'identique, jours de la semaine compris, tous les 400400 ans.

j = 400 * 365 + 97
print(j, j / 400, j % 7, j // 7)
146097 365.2425 0 20871
Rappel de cours

Conditionnelle. if C1: … elif C2: … else: … exécute le bloc de la première condition vraie, et un seul ; le else reçoit tout ce que les conditions ont laissé passer. La branche elif C2 s'exécute si et seulement si C1 est fausse et C2 vraie.

Ordre des tests. Si une condition en entraîne une autre (ici 400∣a⇒100∣a⇒4∣a400\mid a\Rightarrow100\mid a\Rightarrow4\mid a), la plus particulière doit venir en premier ; sinon les suivantes sont inatteignables.

Tester une fonction. Un jeu de tests doit faire passer le programme par chaque branche, et viser les cas limites. assert condition ne fait rien si la condition est vraie et arrête le programme sur une AssertionError sinon.

L'erreur classique

⚠️ Écrire les tests dans l'ordre où on lit la règle. La phrase « divisible par 44, sauf les multiples de 100100, sauf les multiples de 400400 » commence par le cas le plus général ; recopiée telle quelle en cascade de elif, elle produit exactement la fonction de l'élève. Une règle à exceptions se programme en commençant par l'exception la plus rare.

⚠️ Mélanger deux styles. Avec des if indépendants qui modifient une variable, c'est le dernier test vrai qui l'emporte, et l'ordre juste s'inverse : du plus général au plus particulier.

def bis3(a):
    r = False
    if a % 4 == 0:
        r = True
    if a % 100 == 0:
        r = False
    if a % 400 == 0:
        r = True
    return r

for a in [2024, 2023, 1900, 2000]:
    print(a, bis3(a))
2024 True
2023 False
1900 False
2000 True

Écrire ces trois if dans l'ordre 400400, 100100, 44 serait faux ; écrire la cascade d'elif dans l'ordre 44, 100100, 400400 l'est aussi. Chaque style a son ordre : il faut savoir lequel on écrit.

⚠️ Ne tester que des années « faciles ». 20242024 et 20232023 passent avec les deux versions ; ce sont les années séculaires (19001900, 20002000) qui départagent.

Réponse. Tester 400400, puis 100100, puis 44 : la fonction renvoie True, False, False, True pour 20242024, 20232023, 19001900, 20002000. Celle de l'élève renvoie True pour 19001900 (faux) et pour 20002000 (juste par hasard) : tout multiple de 100100 est multiple de 44, ses deux elif sont morts. Jeu de tests : 20002000, 19001900, 20242024, 20232023, une année par branche. De 16011601 à 20002000 : 9797 années bissextiles, soit une année moyenne de 365,2425365{,}2425 jours.
Faire cet exercice dans l'app →

Racines d'un trinôme : trois cas et une garde

DémonstrationDifficulté 2/5

On cherche les racines réelles de ax2+bx+cax^2+bx+c, les coefficients aa, bb, cc étant des entiers.

1. Écrire une fonction racines(a, b, c) qui renvoie la liste des racines réelles dans l'ordre croissant — deux, une ou aucune selon le signe du discriminant Δ=b2−4ac\Delta=b^2-4ac. On utilisera math.sqrt.

2. Donner ce qu'elle renvoie pour x2−3x+2x^2-3x+2, x2−2x+1x^2-2x+1, x2+x+1x^2+x+1 et −x2+3x−2-x^2+3x-2. Pour x2−2x^2-2, reporter les racines obtenues dans le trinôme : trouve-t-on 00 ?

3. Que se passe-t-il pour racines(0, 2, -4) ? Ajouter une garde qui traite le cas a=0a=0.

4. Pourquoi exige-t-on des coefficients entiers ? Examiner le trinôme (x−0,1)2=x2−0,2x+0,01(x-0{,}1)^2=x^2-0{,}2x+0{,}01 saisi avec des flottants, puis écrit 100x2−20x+1100x^2-20x+1.

Indices (3)

Trois cas qui s'excluent : un if, un elif, un else. Pour l'ordre croissant, demande-toi lequel des deux quotients −b±Δ2a\frac{-b\pm\sqrt\Delta}{2a} est le plus petit quand a<0a<0.

Reporter une racine xx dans le trinôme, c'est calculer a * x * x + b * x + c : attends-toi à un résultat très petit, pas forcément nul.

Avec des entiers, b * b - 4 * a * c est calculé exactement et le test d == 0 est fiable. Avec des flottants, 0,220{,}2^2 n'est pas exactement 0,040{,}04.

Correction détaillée
Ce qu'il faut voir

Trois cas qui s'excluent, donc un if, un elif, un else. Le signe du discriminant Δ\Delta décide de tout : Δ>0\Delta>0 donne deux racines, Δ=0\Delta=0 une racine double, Δ<0\Delta<0 aucune racine réelle. La cascade de tests traduit exactement ce tableau de cas, et le else n'a pas besoin de condition : il reçoit Δ<0\Delta<0, le seul cas restant.

Deux détails transforment la formule du cours en programme correct. D'abord une garde : la formule divise par 2a2a, donc elle n'a de sens que si a≠0a\neq0, et le programme doit s'en occuper avant de diviser. Ensuite le test d == 0 : il n'est fiable que si Δ\Delta est calculé exactement, ce qui est le cas avec des entiers Python (ils ne sont jamais arrondis) et ne l'est pas avec des flottants.

👉 Ce qu'on retient : un programme hérite des hypothèses de la formule qu'il code (a≠0a\neq0, Δ\Delta exact). Il faut les écrire noir sur blanc — en garde, en assert, ou dans la description de la fonction.

La fonction : sommet et demi-écart

On écrit les racines −b±Δ2a\dfrac{-b\pm\sqrt\Delta}{2a} sous la forme s±hs\pm h avec

s=−b2a,h=Δ2∣a∣.s=-\frac{b}{2a},\qquad h=\frac{\sqrt\Delta}{2\lvert a\rvert}.
ss est l'abscisse du sommet de la parabole, et les deux racines lui sont symétriques. Comme h≥0h\geq0, la liste [s - h, s + h] est toujours rangée dans l'ordre croissant, même si a<0a<0 : c'est la raison du abs(a).

import math

def racines(a, b, c):
    d = b * b - 4 * a * c
    s = -b / (2 * a)
    if d > 0:
        r = math.sqrt(d)
        h = r / (2 * abs(a))
        return [s - h, s + h]
    elif d == 0:
        return [s]
    else:
        return []

print(racines(1, -3, 2))
print(racines(1, -2, 1))
print(racines(1, 1, 1))
print(racines(-1, 3, -2))
[1.0, 2.0]
[1.0]
[]
[1.0, 2.0]

Vérification des cas. x2−3x+2x^2-3x+2 : Δ=9−8=1\Delta=9-8=1, racines 3±12\frac{3\pm1}{2}, soit 11 et 22 ; on retrouve la somme 1+2=3=−ba1+2=3=-\frac ba et le produit 1×2=2=ca1\times2=2=\frac ca. x2−2x+1=(x−1)2x^2-2x+1=(x-1)^2 : Δ=0\Delta=0, racine double 11. x2+x+1x^2+x+1 : Δ=1−4=−3<0\Delta=1-4=-3<0, liste vide. −x2+3x−2=−(x2−3x+2)-x^2+3x-2=-(x^2-3x+2) a les mêmes racines, et elles sortent bien dans l'ordre croissant. Avec la formule −b±Δ2a\frac{-b\pm\sqrt\Delta}{2a} écrite telle quelle, le dénominateur 2a=−22a=-2 est négatif et l'ordre s'inverse :

x1 = (-3 - 1) / (-2)
x2 = (-3 + 1) / (-2)
print(x1, x2)
2.0 1.0
Contrôler une racine irrationnelle

x2−2x^2-2 a pour racines ±2\pm\sqrt2, qui ne sont pas des décimaux : le programme en donne des valeurs approchées. On les affiche une par une, chacune suivie de son report dans le trinôme.

import math

def racines(a, b, c):
    d = b * b - 4 * a * c
    s = -b / (2 * a)
    if d > 0:
        r = math.sqrt(d)
        h = r / (2 * abs(a))
        return [s - h, s + h]
    elif d == 0:
        return [s]
    else:
        return []

for x in racines(1, 0, -2):
    print(x)
    print(x * x - 2)
-1.4142135623730951
4.440892098500626e-16
1.4142135623730951
4.440892098500626e-16

Le report ne donne pas 00 mais environ 4,4×10−164{,}4\times10^{-16}. La valeur affichée de 2\sqrt2 est le flottant le plus proche de 2\sqrt2, pas 2\sqrt2 lui-même, et son carré dépasse 22 d'une quantité minuscule, de l'ordre de la précision des flottants. Ce n'est pas une erreur du programme : c'est la raison pour laquelle on compare des flottants avec une tolérance, jamais avec == (A4). Le contrôle sûr s'écrit ainsi :

x = 1.4142135623730951
print(abs(x * x - 2) < 1e-12)
True

Le seuil 10−1210^{-12} est très au-dessus de l'erreur d'arrondi (4,4×10−164{,}4\times10^{-16}) et très au-dessous de l'écart que donnerait une racine fausse.

Le cas a = 0 et sa garde

Pour racines(0, 2, -4), le calcul de s divise par 2 * a, qui vaut 00 :

import math

def racines(a, b, c):
    d = b * b - 4 * a * c
    s = -b / (2 * a)
    if d > 0:
        r = math.sqrt(d)
        h = r / (2 * abs(a))
        return [s - h, s + h]
    elif d == 0:
        return [s]
    else:
        return []

print(racines(0, 2, -4))
ZeroDivisionError: division by zero

Le programme s'arrête sur une exception : le print n'est jamais exécuté. Pourtant la question a un sens : 0x2+2x−4=00x^2+2x-4=0 est l'équation du premier degré 2x−4=02x-4=0, de solution x=2x=2. La formule des trinômes ne s'applique pas, et c'est au programme de le voir avant de diviser.

On ajoute une garde en tête de fonction : si a=0a=0, l'équation est bx+c=0bx+c=0, de solution −cb-\frac cb pourvu que b≠0b\neq0. Le cas a=b=0a=b=0 n'est plus une équation du premier degré (elle est vraie pour tout xx si c=0c=0, fausse pour tout xx sinon) : on le refuse par un assert, qui arrête le programme sur une AssertionError désignant la précondition violée, au lieu d'une division par zéro qui n'en est qu'un symptôme.

import math

def racines(a, b, c):
    if a == 0:
        assert b != 0
        return [-c / b]
    d = b * b - 4 * a * c
    s = -b / (2 * a)
    if d > 0:
        r = math.sqrt(d)
        h = r / (2 * abs(a))
        return [s - h, s + h]
    elif d == 0:
        return [s]
    else:
        return []

print(racines(0, 2, -4))
print(racines(1, -3, 2))
[2.0]
[1.0, 2.0]

La garde est placée avant le calcul de s : placée après, elle arriverait trop tard. Et le comportement pour a≠0a\neq0 est inchangé (deuxième ligne).

Pourquoi des coefficients entiers

Le trinôme (x−0,1)2=x2−0,2x+0,01(x-0{,}1)^2=x^2-0{,}2x+0{,}01 a une racine double, 0,10{,}1. Saisi avec des flottants, son discriminant vaut :

b = -0.2
c = 0.01
d = b * b - 4 * c
print(d)
print(d == 0)
6.938893903907228e-18
False

Mathématiquement Δ=0,04−0,04=0\Delta=0{,}04-0{,}04=0 ; en machine, ni 0,20{,}2 ni 0,010{,}01 n'ont d'écriture binaire finie, les deux produits sont arrondis différemment, et il reste un résidu d'environ 7×10−187\times10^{-18}. Le test d == 0 échoue, et la fonction voit deux racines distinctes là où il n'y en a qu'une. Multiplier le trinôme par 100100 ne change pas ses racines et rend les coefficients entiers : 100x2−20x+1100x^2-20x+1 a pour discriminant 400−400=0400-400=0, calculé exactement.

import math

def racines(a, b, c):
    d = b * b - 4 * a * c
    s = -b / (2 * a)
    if d > 0:
        r = math.sqrt(d)
        h = r / (2 * abs(a))
        return [s - h, s + h]
    elif d == 0:
        return [s]
    else:
        return []

for x in racines(1, -0.2, 0.01):
    print(x)
print(racines(100, -20, 1))
0.09999999868291098
0.10000000131708903
[0.1]

Avec les flottants, deux « racines » écartées d'environ 2,6×10−92{,}6\times10^{-9} ; avec les entiers, la racine double 0,10{,}1.

👉 Avec des entiers, le test d == 0 est exact ; avec des flottants, un « zéro » peut sortir positif ou négatif selon les arrondis. Si les coefficients sont décimaux, on les multiplie par une puissance de 1010 pour les rendre entiers — ou l'on remplace d == 0 par abs(d) < eps, en choisissant eps en connaissance de cause.

Rappel de cours

Trinôme. Pour a≠0a\neq0, Δ=b2−4ac\Delta=b^2-4ac. Si Δ>0\Delta>0 : deux racines −b±Δ2a\dfrac{-b\pm\sqrt\Delta}{2a} ; si Δ=0\Delta=0 : une racine double −b2a-\dfrac b{2a} ; si Δ<0\Delta<0 : aucune racine réelle. Somme des racines −ba-\dfrac ba, produit ca\dfrac ca.

En Python. math.sqrt(x) exige x >= 0 (sinon ValueError) : raison de plus pour ne l'appeler que dans la branche Δ>0\Delta>0. Les entiers (int) sont exacts et sans limite de taille ; les flottants (float) sont arrondis, et 0.1 n'est pas exactement 0,10{,}1.

Garde. Tester les cas où la formule n'a pas de sens avant de l'appliquer ; refuser par assert ce que la fonction ne sait pas traiter.

L'erreur classique

⚠️ Mettre la garde après le calcul. Si l'on calcule s = -b / (2 * a) en première ligne et qu'on teste a == 0 ensuite, la division a déjà eu lieu : le programme s'arrête avant d'atteindre la garde. L'ordre des instructions compte autant que l'ordre des tests.

⚠️ Tester d >= 0 avant d == 0. Dans une cascade, le premier test vrai l'emporte : avec if d >= 0: en tête, le cas Δ=0\Delta=0 tombe dans la branche des deux racines et renvoie deux fois la même valeur. Les conditions s'écrivent d > 0, puis d == 0, puis else.

⚠️ Appeler math.sqrt(d) avant de connaître le signe de d. Pour Δ<0\Delta<0, math.sqrt lève une ValueError : la racine carrée ne se calcule que dans la branche où elle existe.

Réponse. Racines s±hs\pm h avec s=−b2as=-\frac b{2a} et h=Δ2∣a∣h=\frac{\sqrt\Delta}{2\lvert a\rvert}, donc rangées d'office : [1.0, 2.0], [1.0], [], et [1.0, 2.0] pour −x2+3x−2-x^2+3x-2. Pour x2−2x^2-2, le report donne 4,4×10−164{,}4\times10^{-16} et non 00 (flottants). Sans garde, racines(0, 2, -4) lève ZeroDivisionError ; avec la garde, [2.0]. Avec 0,20{,}2 et 0,010{,}01 flottants, Δ≈6,9×10−18≠0\Delta\approx6{,}9\times10^{-18}\neq0 ; en entiers, 100x2−20x+1100x^2-20x+1 donne [0.1].
Faire cet exercice dans l'app →

Boucle for et range : compter les tours

DémonstrationDifficulté 2/5

1. Quels entiers parcourt la variable i dans for i in range(5), puis avec range(2, 6), range(1, 10, 3), range(10, 0, -2) et range(5, 2) ? Combien de tours fait for i in range(a, b) lorsque a≤ba\leq b ?

2. Écrire une boucle qui calcule n!=1×2×⋯×nn!=1\times2\times\cdots\times n, puis dresser son tableau de trace pour n=5n=5.

3. Pour calculer 12+22+⋯+n21^2+2^2+\cdots+n^2, un élève écrit :

s = 0
for k in range(1, n):
    s = s + k * k

Qu'obtient-il pour n=10n=10 ? Corriger la boucle et comparer à la formule n(n+1)(2n+1)6\dfrac{n(n+1)(2n+1)}{6}.

4. Vérifier par programme que la boucle corrigée et la formule coïncident pour tous les nn de 00 à 100100.

Indices (3)

range(a, b) commence à a et s'arrête avant b ; le troisième argument est le pas, qui peut être négatif.

Pour un produit, l'accumulateur part de 11, pas de 00. Dans le tableau de trace, écris l'état des variables après chaque tour.

Compare le dernier terme ajouté par la boucle de l'élève au dernier terme de la somme voulue.

Correction détaillée
Ce qu'il faut voir

range(a, b) parcourt les entiers de aa à b−1b-1 : le début est inclus, la fin est exclue. Donc range(n), qui abrège range(0, n), donne les nn entiers 0,1,…,n−10,1,\dots,n-1 — exactement ce qu'il faut pour parcourir les indices d'une liste de longueur nn. Mais pour parcourir 1,2,…,n1,2,\dots,n, il faut écrire range(1, n + 1), et c'est là que se glisse l'erreur la plus fréquente des boucles : oublier le dernier terme.

La convention a une vertu : range(a, b) fait exactement b−ab-a tours (quand a≤ba\leq b), sans « +1+1 » à retenir, et deux plages consécutives range(a, b) et range(b, c) se recollent sans chevauchement en range(a, c).

👉 Ce qu'on retient : avant d'écrire un range, se demander quel est le dernier entier voulu, et mettre son successeur comme borne de fin. Puis vérifier sur un petit nn, en traçant la boucle.

Ce que parcourt range

On fait afficher chaque range converti en liste par list :

print(list(range(5)))
print(list(range(2, 6)))
print(list(range(1, 10, 3)))
print(list(range(10, 0, -2)))
print(list(range(5, 2)))
print(list(range(1, 11, 3)))
[0, 1, 2, 3, 4]
[2, 3, 4, 5]
[1, 4, 7]
[10, 8, 6, 4, 2]
[]
[1, 4, 7, 10]

range(5) donne les 55 entiers de 00 à 44 ; range(2, 6) en donne 6−2=46-2=4. Avec un pas de 33, on part de 11 et on ajoute 33 tant qu'on reste strictement sous 1010 : 11, 44, 77, et 1010 est exclu. Avec un pas négatif, on descend tant qu'on reste strictement au-dessus de la borne : 1010, 88, 66, 44, 22, et 00 est exclu. Enfin range(5, 2) est vide : ce n'est pas une erreur, la boucle fait simplement zéro tour.

Compter les tours. On le vérifie avec un compteur, sur un cas où la réponse n'est pas évidente à l'œil :

t = 0
for i in range(3, 20):
    t = t + 1
print(t)
17

On trouve bien 20−3=1720-3=17 tours. Avec un pas p>0p>0, range(a, b, p) parcourt les entiers a+kp<ba+kp<b (kk entier naturel), soit ⌈b−ap⌉\left\lceil\frac{b-a}{p}\right\rceil tours si a≤ba\leq b, et aucun sinon : la dernière ligne du premier programme, range(1, 11, 3), parcourt 11, 44, 77, 1010, soit ⌈103⌉=4\left\lceil\frac{10}{3}\right\rceil=4 tours.

La factorielle et sa trace

Un produit s'accumule dans une variable qui part de 11, l'élément neutre de la multiplication, comme une somme part de 00 :

n = 5
f = 1
for i in range(1, n + 1):
    f = f * i
print(f)
120

Tableau de trace pour n=5n=5 : l'état des variables avant la boucle, puis après chaque tour.

étape i f
avant — 1
après le tour 1 1 1
après le tour 2 2 2
après le tour 3 3 6
après le tour 4 4 24
après le tour 5 5 120

Avant la boucle, i n'existe pas encore : la variable d'une boucle for est créée au premier tour. La boucle fait (n+1)−1=5(n+1)-1=5 tours, et après le tour ii la variable f vaut i!i! : c'est vrai après le tour 11 (1=1!1=1!), et si f vaut (i−1)!(i-1)! au début du tour ii, elle vaut (i−1)!×i=i!(i-1)!\times i=i! à la fin. En sortie, f vaut donc n!n!. Ce raisonnement est un invariant de boucle, que l'approfondissement systématise (C1).

Recoupement sur plusieurs valeurs, dont le cas limite n=0n=0 :

for n in [0, 1, 5, 10]:
    f = 1
    for i in range(1, n + 1):
        f = f * i
    print(n, f)
0 1
1 1
5 120
10 3628800

Pour n=0n=0, range(1, 1) est vide : zéro tour, et f reste à 11, conformément à la convention 0!=10!=1. 10!=3 628 80010!=3\,628\,800, et les entiers Python n'ont pas de limite de taille : la même boucle donne 100!100! exactement, sans dépassement.

La borne oubliée

On lance la boucle de l'élève pour n=10n=10 :

n = 10
s = 0
for k in range(1, n):
    s = s + k * k
print(s)
285

La formule donne 10×11×216=385\dfrac{10\times11\times21}{6}=385. L'écart vaut 385−285=100=102385-285=100=10^2 : exactement le dernier terme. range(1, n) parcourt 1,…,n−11,\dots,n-1 et s'arrête avant nn ; la boucle fait n−1=9n-1=9 tours au lieu de 1010. Le programme ne plante pas et n'affiche aucun message : il donne un résultat plausible et faux, ce qui rend l'erreur dangereuse.

Correction : la borne de fin est le successeur du dernier entier voulu.

n = 10
s = 0
for k in range(1, n + 1):
    s = s + k * k
f = n * (n + 1) * (2 * n + 1) // 6
print(s, f)
385 385

On calcule la formule avec // : le produit n(n+1)(2n+1)n(n+1)(2n+1) est toujours divisible par 66, la division entière est donc exacte et garde un résultat entier, comparable à s sans arrondi.

La formule sur cent et une valeurs

Un exemple ne prouve pas l'égalité, mais cent et un exemples réussis forment un bon contrôle — et un seul échec suffirait à réfuter. On reprend la boucle corrigée pour chaque nn de 00 à 100100, dans une boucle extérieure, et on confronte à la formule par assert :

for n in range(0, 101):
    s = 0
    for k in range(1, n + 1):
        s = s + k * k
    g = n * (n + 1) * (2 * n + 1)
    assert s == g // 6
print('101 valeurs : ok')
101 valeurs : ok

Le print final n'est atteint que si aucun assert n'a échoué. Pour n=0n=0, la boucle intérieure range(1, 1) est vide, s reste à 00 et la formule donne 00 : le cas limite passe aussi.

Un contrôle doit savoir échouer. Le même programme avec la boucle de l'élève, et n ajouté à l'assert pour qu'il affiche la première valeur fautive :

for n in range(0, 101):
    s = 0
    for k in range(1, n):
        s = s + k * k
    g = n * (n + 1) * (2 * n + 1)
    assert s == g // 6, n
print('101 valeurs : ok')
AssertionError: 1

Il s'arrête dès n=1n=1 : range(1, 1) est vide, la somme vaut 00 au lieu de 12=11^2=1. C'est ce second programme qui prouve que le premier teste vraiment quelque chose.

Rappel de cours

La fonction range. range(b) parcourt 0,1,…,b−10,1,\dots,b-1 ; range(a, b) parcourt a,…,b−1a,\dots,b-1 (b−ab-a entiers si a≤ba\leq b, aucun sinon) ; range(a, b, p) avance de pp et s'arrête avant d'atteindre ou de dépasser bb, un pas négatif faisant descendre. La borne de fin est toujours exclue.

Accumulateur. Une somme part de 00, un produit de 11 ; à chaque tour, s = s + terme ou f = f * terme. Pour ∑k=1n\sum_{k=1}^{n}, la boucle est for k in range(1, n + 1).

Trace. Le tableau de trace donne l'état des variables avant la boucle, puis après chaque tour : c'est l'outil pour vérifier une boucle sur un petit cas.

L'erreur classique

⚠️ Le décalage d'une unité aux bornes. range(1, n) oublie nn, range(n) commence à 00 : pour 1,…,n1,\dots,n on écrit range(1, n + 1). L'erreur ne produit ni plantage ni message, seulement une somme fausse ; seuls un calcul de contrôle ou une trace la révèlent.

⚠️ Initialiser un produit à 00. f = 0 puis f = f * i donne 00 à tous les tours : le résultat vaut 00 quel que soit nn.

⚠️ Modifier la variable de boucle dans le corps. Écrire i = i + 1 dans une boucle for ne fait sauter aucun tour : au tour suivant, i reprend la valeur suivante de range.

t = 0
for i in range(5):
    i = i + 1
    t = t + 1
print(t)
5

Cinq tours, comme sans la ligne i = i + 1. Pour contrôler soi-même l'avancée d'un indice, on utilise une boucle while (B4).

Réponse. range(a, b) parcourt a,…,b−1a,\dots,b-1, soit b−ab-a tours si a≤ba\leq b : range(1, 10, 3) donne 1,4,71,4,7, range(10, 0, -2) donne 10,8,6,4,210,8,6,4,2, range(5, 2) est vide. Factorielle : f part de 11, la trace donne 1,2,6,24,1201,2,6,24,120, et 10!=3 628 80010!=3\,628\,800. La boucle range(1, n) affiche 285285 pour n=10n=10 : il manque 10210^2 ; corrigée en range(1, n + 1), elle donne 385385, comme la formule, et coïncide avec elle pour n=0,…,100n=0,\dots,100.
Faire cet exercice dans l'app →

Algorithme de seuil : la boucle while

DémonstrationDifficulté 3/5

Un capital de 1 0001\,000 euros est placé à 5 %5\,\% par an, à intérêts composés : on note u0=1000u_0=1000 et un+1=1,05 unu_{n+1}=1{,}05\,u_n.

1. Écrire un programme qui affiche le premier rang nn tel que un≥2000u_n\geq2000, et la valeur de unu_n à ce rang. Dresser le tableau de trace de la même boucle pour le seuil 1 2001\,200.

2. Un élève recopie la condition de l'énoncé et écrit while u >= 2000:. Qu'affiche son programme ?

3. Pour v0=1v_0=1 et vn+1=2vnv_{n+1}=2v_n, comparer les rangs obtenus avec while v < 8: et avec while v <= 8:. Lequel répond à « premier rang où vn≥8v_n\geq8 » ?

4. Expliquer pourquoi cette boucle ne s'arrête jamais :

u = 1000
n = 0
while u < 2000:
    n = n + 1

5. Retrouver le rang de la question 1 par le logarithme.

Indices (3)

La condition d'un while est celle qui fait continuer : pour s'arrêter au premier un≥2000u_n\geq2000, on boucle tant que un<2000u_n<2000.

Fais avancer n au même tour que u : après chaque tour, u doit contenir unu_n pour la valeur courante de n.

Une boucle while ne s'arrête que si sa condition finit par devenir fausse : quelle instruction du corps fait évoluer u ?

Correction détaillée
Ce qu'il faut voir

Une boucle while répète son corps tant que sa condition est vraie ; on l'utilise quand on ne sait pas d'avance combien de tours il faudra. C'est la situation d'un algorithme de seuil : on calcule les termes un par un et on s'arrête au premier qui franchit le seuil, sans connaître ce rang à l'avance.

Deux réflexes font un algorithme de seuil correct. D'abord, la condition du while est la négation de la condition d'arrêt : on veut s'arrêter dès que un≥2000u_n\geq2000, donc on continue tant que un<2000u_n<2000. Ensuite, le compteur et le terme avancent ensemble : à la fin de chaque tour, u doit contenir unu_n pour la valeur courante de n. Si ce lien est tenu, la valeur de n en sortie de boucle est le rang cherché, sans « +1+1 » ni « −1-1 » à ajouter.

👉 Ce qu'on retient : écrire la condition de continuation, faire évoluer à chaque tour la variable qui y figure, et vérifier le résultat par un autre chemin — ici, le logarithme.

Le premier rang où le capital double

La boucle continue tant que le seuil n'est pas atteint :

u = 1000
n = 0
while u < 2000:
    u = 1.05 * u
    n = n + 1
print(n, u)
15 2078.9281794113685

Le capital dépasse 2 0002\,000 euros pour la première fois au rang n=15n=15, où il vaut environ 2 078,932\,078{,}93 euros ; la question 5 vérifie que le rang 1414 ne convient pas.

Trace pour le seuil 1 2001\,200, qui demande peu de tours :

u = 1000
n = 0
while u < 1200:
    u = 1.05 * u
    n = n + 1
print(n, u)
4 1215.5062500000001
étape n u
avant 0 1000
après le tour 1 1 1050.0
après le tour 2 2 1102.5
après le tour 3 3 1157.625
après le tour 4 4 1215.5062500000001

Au test suivant, la condition u < 1200 est fausse : la boucle s'arrête avec n=4n=4. On lit sur la trace le lien que la boucle maintient : après chaque tour, u vaut unu_n — u1=1050u_1=1050, u2=1102,5u_2=1102{,}5, u3=1157,625u_3=1157{,}625, u4=1215,50625u_4=1215{,}50625. La dernière valeur s'affiche 1215.5062500000001 et non 1215.50625 : 1,051{,}05 n'a pas d'écriture binaire finie, et les produits successifs sont arrondis (A4). L'écart, au dix-septième chiffre significatif, ne change pas le rang.

Recopier la condition d'arrêt : zéro tour

Avec la condition de l'énoncé à la place de sa négation :

u = 1000
n = 0
while u >= 2000:
    u = 1.05 * u
    n = n + 1
print(n, u)
0 1000

Au premier test, u=1000u=1000 et u >= 2000 est faux : le corps n'est jamais exécuté, et le programme affiche le rang 00 avec le capital initial. Une boucle while peut faire zéro tour ; c'est même le premier cas à vérifier. La condition d'un while dit quand continuer, pas quand s'arrêter : on y écrit la négation de « un≥2000u_n\geq2000 », c'est-à-dire u < 2000.

Le seuil atteint exactement

Tant qu'aucun terme ne tombe pile sur le seuil, < et <= donnent le même rang ; la différence apparaît dans le cas d'égalité. Avec v0=1v_0=1, vn+1=2vnv_{n+1}=2v_n et le seuil 8=v38=v_3 :

v = 1
n = 0
while v < 8:
    v = 2 * v
    n = n + 1
print(n, v)

v = 1
n = 0
while v <= 8:
    v = 2 * v
    n = n + 1
print(n, v)
3 8
4 16

Avec while v < 8, la boucle s'arrête dès que vn≥8v_n\geq8 : rang 33, avec v3=8v_3=8. Avec while v <= 8, elle continue quand v=8v=8 et ne s'arrête qu'au premier vn>8v_n>8 : rang 44. La première répond à « premier rang où vn≥8v_n\geq8 », la seconde à « premier rang où vn>8v_n>8 ». Ces deux questions ont des réponses décalées d'une unité, et c'est l'énoncé — pas le programme — qui dit laquelle est posée. Ici les calculs se font en entiers, donc exactement, et le cas d'égalité est fiable ; avec des flottants, il pourrait être manqué pour une erreur d'arrondi.

Une boucle qui ne s'arrête jamais

Le programme de la question 4 ne termine pas : le banc de ce chapitre l'a lancé et l'a trouvé encore en train de tourner au bout de deux secondes.

u = 1000
n = 0
while u < 2000:
    n = n + 1

La cause : le corps ne modifie jamais u. La condition u < 2000 est vraie au premier test, et comme rien ne change u, elle reste vraie à tous les tests suivants ; seul n augmente, indéfiniment. La règle : chaque tour doit rapprocher la condition de devenir fausse.

Il ne suffit pas que la variable change : il faut qu'elle finisse par franchir le seuil. Second piège, plus sournois — un seuil que la suite n'atteint jamais :

u = 1000
n = 0
while u < 2000:
    u = 0.5 * u + 900
    n = n + 1

Ici u change à chaque tour, mais la suite un+1=0,5 un+900u_{n+1}=0{,}5\,u_n+900 croît vers 1 8001\,800, le point fixe de x↦0,5x+900x\mapsto0{,}5x+900, en restant en dessous : elle n'atteint jamais 2 0002\,000, et la boucle tourne sans fin. Avant d'écrire un algorithme de seuil, il faut savoir — par le cours sur les suites — que le seuil sera franchi. Pour un=1000×1,05nu_n=1000\times1{,}05^n, c'est acquis puisque 1,05n→+∞1{,}05^n\to+\infty (raison strictement supérieure à 11).

Le rang par le logarithme

(un)(u_n) est géométrique de raison 1,051{,}05 : un=1000×1,05nu_n=1000\times1{,}05^n. Comme ln⁡\ln est croissante et ln⁡1,05>0\ln1{,}05>0,

un≥2000  ⟺  1,05n≥2  ⟺  nln⁡1,05≥ln⁡2  ⟺  n≥ln⁡2ln⁡1,05.u_n\geq2000\iff1{,}05^n\geq2\iff n\ln1{,}05\geq\ln2\iff n\geq\frac{\ln2}{\ln1{,}05}.

import math

q = math.log(2) / math.log(1.05)
print(q)
print(1000 * 1.05 ** 14)
print(1000 * 1.05 ** 15)
14.206699082890461
1979.9315994393987
2078.9281794113685

Le quotient vaut environ 14,20714{,}207 : le plus petit entier qui le dépasse est 1515, le rang trouvé par la boucle. ✓ Et u14≈1 979,93<2000≤u15≈2 078,93u_{14}\approx1\,979{,}93<2000\leq u_{15}\approx2\,078{,}93 confirme que 1515 est bien le premier rang. La valeur de u15u_{15} calculée directement par 1000 * 1.05 ** 15 coïncide ici avec celle de la boucle.

La boucle garde un avantage sur le logarithme : elle marche aussi pour des suites dont on ne connaît pas de formule explicite, comme un+1=un+unu_{n+1}=u_n+\sqrt{u_n}. Le logarithme, lui, sert de contrôle quand la formule existe.

Rappel de cours

Boucle while. while condition: teste la condition avant chaque tour ; si elle est fausse dès le départ, le corps n'est jamais exécuté. On l'emploie quand le nombre de tours n'est pas connu d'avance.

Algorithme de seuil. Pour le premier rang nn tel que un≥Su_n\geq S : initialiser u à u0u_0 et n à 00 ; tant que u < S, remplacer u par le terme suivant et augmenter n de 11. En sortie, n est le rang cherché et u vaut unu_n.

Terminaison. La boucle s'arrête si la suite franchit effectivement le seuil (par exemple si un→+∞u_n\to+\infty) ; si la variable de la condition ne change pas, ou si la suite converge sans atteindre le seuil, elle tourne sans fin.

L'erreur classique

⚠️ Écrire la condition d'arrêt au lieu de la condition de continuation. while u >= 2000 en voulant « s'arrêter quand u≥2000u\geq2000 » : la boucle fait zéro tour (question 2). On traduit « jusqu'à ce que PP » par while not P, ou par la négation écrite en clair.

⚠️ Corriger le rang « à la main ». Afficher n - 1 ou n + 1 parce que « ça semble décalé » masque une erreur au lieu de la corriger. Si u et n avancent ensemble dans le corps, n est juste en sortie ; la trace sur un petit seuil le montre.

⚠️ Oublier de mettre à jour la variable testée. Une boucle while dont le corps ne modifie pas la variable de sa condition ne s'arrête jamais (question 4). Même chose si l'on écrit 1.05 * u sans l'affecter à u.

Réponse. Premier rang : n=15n=15, avec u15≈2 078,93u_{15}\approx2\,078{,}93 (et u14≈1 979,93u_{14}\approx1\,979{,}93). Pour le seuil 1 2001\,200, la trace donne n=4n=4 après 10501050, 1102,51102{,}5, 1157,6251157{,}625, 1215,506251215{,}50625. while u >= 2000 fait zéro tour et affiche 0 1000. Pour vn=2nv_n=2^n : < donne le rang 33 (vn≥8v_n\geq8), <= le rang 44 (vn>8v_n>8). Sans mise à jour de u, la boucle ne s'arrête pas. Logarithme : n≥ln⁡2/ln⁡1,05≈14,207n\geq\ln2/\ln1{,}05\approx14{,}207, donc n=15n=15.
Faire cet exercice dans l'app →

Compteurs et accumulateurs : diviseurs et nombres parfaits

DémonstrationDifficulté 2/5

1. Écrire un programme qui, pour n=36n=36, compte les diviseurs positifs de nn et calcule leur somme, dans une seule boucle.

2. Un entier n≥2n\geq2 est parfait s'il est égal à la somme de ses diviseurs positifs autres que lui-même, comme 6=1+2+36=1+2+3. Écrire une fonction somme_div(n) qui renvoie cette somme, et chercher les nombres parfaits inférieurs à 1 0001\,000.

3. Un élève compte les diviseurs de 3636 ainsi :

n = 36
for d in range(1, n + 1):
    c = 0
    if n % d == 0:
        c = c + 1

Que vaut c à la fin ? Pourquoi ? Et pour n=35n=35 ?

4. Retrouver le nombre et la somme des diviseurs de 3636 à partir de sa décomposition 36=22×3236=2^2\times3^2.

Indices (3)

Un compteur augmente de 11 à chaque cas favorable, un accumulateur ajoute une quantité ; les deux s'initialisent avant la boucle.

Pour les diviseurs autres que nn, la boucle s'arrête avant nn : quelle borne mettre dans range ?

Regarde quelle instruction s'exécute à chaque tour, et ce qui reste dans c après le dernier tour.

Correction détaillée
Ce qu'il faut voir

Un compteur et un accumulateur sont des variables qui résument ce que la boucle a vu jusqu'ici. Le compteur c répond à « combien de fois ? » : il part de 00 et augmente de 11 à chaque cas favorable. L'accumulateur s répond à « combien en tout ? » : il part de 00 (ou de 11 pour un produit) et reçoit une quantité à chaque cas favorable. Les deux suivent la même structure en trois temps : initialiser avant la boucle, mettre à jour dans la boucle, lire après la boucle.

L'initialisation a lieu une seule fois, avant le premier tour. Placée dans la boucle, elle efface à chaque tour ce que les tours précédents avaient accumulé, et la variable ne résume plus que le dernier tour.

👉 Ce qu'on retient : pour chaque variable de résumé, se demander « que doit-elle valoir avant le premier tour ? » — la réponse pour « aucun élément vu » — et écrire cette valeur au-dessus de la boucle.

Compter et sommer dans la même boucle

On parcourt les candidats d=1,…,nd=1,\dots,n ; dd divise nn si et seulement si le reste n % d est nul.

n = 36
c = 0
s = 0
for d in range(1, n + 1):
    if n % d == 0:
        c = c + 1
        s = s + d
print(c, s)
9 91

3636 a 99 diviseurs positifs, de somme 9191. Les deux mises à jour sont dans le même if : on compte et on ajoute exactement les mêmes dd. La boucle fait 3636 tours, un par candidat.

Contrôle par la liste. Pour vérifier, on construit la liste des diviseurs :

n = 36
t = []
for d in range(1, n + 1):
    if n % d == 0:
        t.append(d)
print(t)
print(len(t), sum(t))
[1, 2, 3, 4, 6, 9, 12, 18, 36]
9 91

Les diviseurs vont par paires dd et 36/d36/d — 11 et 3636, 22 et 1818, 33 et 1212, 44 et 99 —, sauf 66, apparié à lui-même : c'est parce que 3636 est un carré que son nombre de diviseurs est impair.

Les nombres parfaits

Pour les diviseurs stricts (autres que nn), la boucle s'arrête avant nn : range(1, n). On enferme le calcul dans une fonction, puis on l'appelle pour chaque candidat :

def somme_div(n):
    s = 0
    for d in range(1, n):
        if n % d == 0:
            s = s + d
    return s

for n in range(2, 1000):
    if somme_div(n) == n:
        print(n)
6
28
496

Trois nombres parfaits sous 1 0001\,000 : 6=1+2+36=1+2+3, 28=1+2+4+7+1428=1+2+4+7+14 et 496=1+2+4+8+16+31+62+124+248496=1+2+4+8+16+31+62+124+248. Le suivant est 8 1288\,128, qu'on vérifie directement :

def somme_div(n):
    s = 0
    for d in range(1, n):
        if n % d == 0:
            s = s + d
    return s

print(somme_div(8128))
8128

Coût. La recherche sous 1 0001\,000 exécute le test n % d == 0 une fois pour chaque couple (n,d)(n,d) avec 1≤d<n1\leq d<n, soit 1+2+⋯+998=998×9992=498 5011+2+\cdots+998=\frac{998\times999}{2}=498\,501 fois. Poussée jusqu'à 10 00010\,000, elle en ferait près de cent fois plus : c'est pourquoi on la limite ici. Un premier gain est facile — aucun diviseur strict de nn ne dépasse n/2n/2, donc range(1, n // 2 + 1) suffit et divise le travail par deux.

L'initialisation dans la boucle

On lance le programme de l'élève, en affichant c à la fin, pour 3636 puis 3535 :

for n in [36, 35]:
    for d in range(1, n + 1):
        c = 0
        if n % d == 0:
            c = c + 1
    print(n, c)
36 1
35 1

La ligne c = 0 est dans la boucle : elle s'exécute à chaque tour, avant le test. Au tour dd, c repart de 00, puis vaut 11 si dd divise nn et 00 sinon. Après le dernier tour, c ne dit donc qu'une chose : si le dernier candidat, d=nd=n, divise nn. Or nn divise toujours nn : le programme affiche 11 pour tout nn, que celui-ci ait 99 diviseurs comme 3636 ou 44 comme 35=5×735=5\times7. Un résultat qui ne dépend pas de la donnée est un signal d'alarme.

Correction : sortir c = 0 de la boucle et le placer au-dessus du for — c'est le programme de la question 1.

Recoupement par la décomposition

Un diviseur positif de 36=22×3236=2^2\times3^2 s'écrit 2i×3j2^i\times3^j avec 0≤i≤20\leq i\leq2 et 0≤j≤20\leq j\leq2, et deux couples (i,j)(i,j) différents donnent deux diviseurs différents (unicité de la décomposition, vue en arithmétique). D'où (2+1)(2+1)=9(2+1)(2+1)=9 diviseurs, et leur somme se factorise :

∑i=02∑j=022i 3j=(1+2+4)(1+3+9)=7×13=91.\sum_{i=0}^{2}\sum_{j=0}^{2}2^i\,3^j=(1+2+4)(1+3+9)=7\times13=91.
Les deux valeurs du programme sont retrouvées. ✓ Le calcul se programme par deux boucles emboîtées, une par exposant (B6) :

c = 0
s = 0
for i in range(3):
    for j in range(3):
        c = c + 1
        s = s + 2 ** i * 3 ** j
print(c, s)
9 91

La même méthode donne, pour 28=22×728=2^2\times7, une somme de tous les diviseurs égale à (1+2+4)(1+7)=56=2×28(1+2+4)(1+7)=56=2\times28 : un nombre est parfait exactement quand la somme de tous ses diviseurs vaut le double de lui-même.

Rappel de cours

Compteur. c = 0 avant la boucle ; c = c + 1 à chaque cas favorable ; en sortie, c est le nombre de cas favorables.

Accumulateur. s = 0 (somme) ou p = 1 (produit) avant la boucle ; s = s + x ou p = p * x à chaque tour concerné.

Divisibilité en Python. Pour d non nul, d divise n si et seulement si n % d == 0. Les diviseurs positifs de nn sont parmi 1,…,n1,\dots,n ; ses diviseurs stricts parmi 1,…,n−11,\dots,n-1, et même parmi 1,…,⌊n/2⌋1,\dots,\lfloor n/2\rfloor.

L'erreur classique

⚠️ Initialiser dans la boucle. c = 0 écrit sous le for remet le compteur à zéro à chaque tour : le résultat ne reflète que le dernier tour (question 3). La valeur initiale se pose une fois, avant la boucle.

⚠️ Oublier d'initialiser. Sans c = 0 avant la boucle, la première exécution de c = c + 1 lève une NameError : Python ne sait pas ce que vaut c.

⚠️ Compter nn parmi ses propres diviseurs stricts. Avec range(1, n + 1) dans somme_div, la somme contient 11 et nn, donc dépasse nn : le test somme_div(n) == n ne réussit plus jamais, et la recherche ne trouve aucun nombre parfait.

Réponse. 3636 a 99 diviseurs, de somme 9191 : un compteur et un accumulateur, initialisés avant la boucle et mis à jour dans le même if. Nombres parfaits sous 1 0001\,000 : 66, 2828, 496496 ; le suivant, 8 1288\,128, se vérifie par somme_div(8128). Avec c = 0 dans la boucle, le programme affiche 11 pour tout nn : seul le tour d=nd=n compte. Décomposition 36=22×3236=2^2\times3^2 : (2+1)(2+1)=9(2+1)(2+1)=9 diviseurs et (1+2+4)(1+3+9)=91(1+2+4)(1+3+9)=91.
Faire cet exercice dans l'app →

Boucles imbriquées : compter les exécutions

DémonstrationDifficulté 3/5

1. Qu'affiche ce programme ? Combien de fois la ligne ligne.append(i * j) est-elle exécutée ?

for i in range(1, 6):
    ligne = []
    for j in range(1, 6):
        ligne.append(i * j)
    print(ligne)

2. Combien de tours fait la boucle intérieure de ce programme, au total, pour n=10n=10 ? pour nn quelconque ?

c = 0
for i in range(n):
    for j in range(i + 1, n):
        c = c + 1

3. Afficher les triplets pythagoriciens (a,b,c)(a,b,c) avec a<b<c≤30a<b<c\leq30 et a2+b2=c2a^2+b^2=c^2, et compter combien de fois le test a2+b2=c2a^2+b^2=c^2 est effectué.

4. Réduire ce nombre de tests en supprimant la boucle sur cc.

Indices (3)

Le corps de la boucle intérieure s'exécute une fois pour chaque couple de valeurs parcouru : on multiplie les nombres de tours si la boucle intérieure ne dépend pas de i, on additionne sinon.

Pour ii fixé, range(i + 1, n) fait n−1−in-1-i tours ; il reste à sommer sur ii.

Trois boucles emboîtées : c, puis b de 11 à c−1c-1, puis a de 11 à b−1b-1. Chaque test correspond à une partie à trois éléments de {1,…,30}\{1,\dots,30\}.

Correction détaillée
Ce qu'il faut voir

Dans deux boucles emboîtées, le corps intérieur s'exécute une fois pour chaque couple de valeurs des deux variables. La boucle extérieure fixe i ; pour ce i, la boucle intérieure fait tous ses tours ; puis on passe au i suivant, et la boucle intérieure recommence depuis le début.

Compter les exécutions du corps intérieur, c'est donc compter des couples. Si la boucle intérieure ne dépend pas de i — pp tours pour chacun des qq tours extérieurs —, on multiplie : pqpq. Si elle en dépend, on additionne son nombre de tours pour chaque valeur de i. Ce nombre mesure le coût du programme : doubler nn double le travail d'une boucle simple, mais le multiplie à peu près par 44 pour deux boucles emboîtées sur nn valeurs.

👉 Ce qu'on retient : pour des boucles imbriquées, écrire le nombre de tours intérieurs en fonction de la variable extérieure, puis sommer — et vérifier la somme avec un compteur.

La table de multiplication

On déroule les deux boucles :

for i in range(1, 6):
    ligne = []
    for j in range(1, 6):
        ligne.append(i * j)
    print(ligne)
[1, 2, 3, 4, 5]
[2, 4, 6, 8, 10]
[3, 6, 9, 12, 15]
[4, 8, 12, 16, 20]
[5, 10, 15, 20, 25]

Chaque tour de la boucle extérieure construit une ligne : ligne repart de la liste vide au début du tour — ici c'est voulu, contrairement au compteur de B5, car on veut une ligne neuve pour chaque i. La boucle intérieure la remplit avec les 55 produits i×ji\times j, puis print l'affiche. La boucle extérieure fait 55 tours ; pour chacun, la boucle intérieure en fait 55 : la ligne ligne.append(i * j) est exécutée 5×5=255\times5=25 fois, et print 55 fois.

On le vérifie en comptant :

c = 0
for i in range(1, 6):
    for j in range(1, 6):
        c = c + 1
print(c)
25
Les couples i < j

Pour ii fixé, range(i + 1, n) parcourt i+1,…,n−1i+1,\dots,n-1, soit n−1−in-1-i tours. En sommant sur i=0,…,n−1i=0,\dots,n-1 :

(n−1)+(n−2)+⋯+1+0=n(n−1)2.(n-1)+(n-2)+\cdots+1+0=\frac{n(n-1)}{2}.
Le corps s'exécute une fois pour chaque couple (i,j)(i,j) avec 0≤i<j≤n−10\leq i<j\leq n-1, c'est-à-dire une fois par partie à deux éléments de {0,…,n−1}\{0,\dots,n-1\} : on retrouve (n2)=n(n−1)2\binom n2=\frac{n(n-1)}2.

for n in [10, 20, 40]:
    c = 0
    for i in range(n):
        for j in range(i + 1, n):
            c = c + 1
    print(n, c, n * (n - 1) // 2)
10 45 45
20 190 190
40 780 780

Pour n=10n=10, 4545 tours. Quand nn double, le nombre de tours est multiplié par environ 44 (190/45≈4,2190/45\approx4{,}2, puis 780/190≈4,1780/190\approx4{,}1) : c'est le coût quadratique d'une double boucle.

Variante. Avec range(i, n) au lieu de range(i + 1, n), on autorise j=ij=i et l'on compte les couples i≤ji\leq j : nn tours de plus, soit n(n+1)2=55\frac{n(n+1)}2=55 pour n=10n=10.

Les triplets pythagoriciens

Trois boucles emboîtées énumèrent les triplets a<b<c≤30a<b<c\leq30 ; le compteur t compte les tests effectués.

t = 0
for c in range(1, 31):
    for b in range(1, c):
        for a in range(1, b):
            t = t + 1
            if a*a + b*b == c*c:
                print(a, b, c)
print('tests :', t)
3 4 5
6 8 10
5 12 13
9 12 15
8 15 17
12 16 20
15 20 25
7 24 25
10 24 26
20 21 29
18 24 30
tests : 4060

On obtient 1111 triplets, rangés par cc croissant parce que c est la variable de la boucle extérieure. Cinq sont primitifs (sans diviseur commun aux trois nombres) : (3,4,5)(3,4,5), (5,12,13)(5,12,13), (8,15,17)(8,15,17), (7,24,25)(7,24,25), (20,21,29)(20,21,29) ; les six autres sont des multiples de (3,4,5)(3,4,5) ou de (5,12,13)(5,12,13).

Le test est effectué une fois par triplet a<b<ca<b<c d'éléments de {1,…,30}\{1,\dots,30\}, soit

(303)=30×29×286=4 060\binom{30}3=\frac{30\times29\times28}{6}=4\,060
fois : c'est exactement ce que compte t. ✓ Pour cc fixé, les deux boucles intérieures font (c−1)(c−2)2\frac{(c-1)(c-2)}{2} tours, et la somme de ces nombres pour c=1,…,30c=1,\dots,30 redonne 4 0604\,060.

Supprimer une boucle

cc est déterminé par aa et bb : c=a2+b2c=\sqrt{a^2+b^2}. Il suffit donc de parcourir les couples a<ba<b et de tester si a2+b2a^2+b^2 est un carré parfait. Comme b<c≤30b<c\leq30, b s'arrête à 2929. On arrondit la racine flottante à l'entier le plus proche, puis on vérifie en entiers que son carré redonne a2+b2a^2+b^2 : le test final ne dépend ainsi d'aucun arrondi.

import math

t = 0
for b in range(1, 30):
    for a in range(1, b):
        t = t + 1
        s = a * a + b * b
        c = round(math.sqrt(s))
        if c * c == s and c <= 30:
            print(a, b, c)
print('tests :', t)
3 4 5
6 8 10
5 12 13
9 12 15
8 15 17
12 16 20
15 20 25
20 21 29
7 24 25
10 24 26
18 24 30
tests : 406

Les mêmes 1111 triplets, rangés cette fois par bb croissant, avec 406406 tests au lieu de 4 0604\,060 : (292)=406\binom{29}2=406 couples a<b≤29a<b\leq29, dix fois moins de travail. Supprimer une boucle en calculant ce qu'elle cherchait est le gain le plus fréquent en algorithmique.

Rappel de cours

Boucles imbriquées. Le corps intérieur s'exécute une fois pour chaque combinaison des variables. Boucles indépendantes : on multiplie les nombres de tours (p×qp\times q). Boucle intérieure dépendant de la variable extérieure : on additionne, pour chaque valeur extérieure, le nombre de tours intérieurs.

Sommes à connaître. ∑i=0n−1(n−1−i)=n(n−1)2=(n2)\sum_{i=0}^{n-1}(n-1-i)=\dfrac{n(n-1)}2=\binom n2 couples i<ji<j ; (n3)\binom n3 triplets a<b<ca<b<c.

Coût. Une boucle sur nn valeurs : de l'ordre de nn opérations ; deux boucles emboîtées : de l'ordre de n2n^2 ; trois : de l'ordre de n3n^3. Diminuer le nombre de boucles emboîtées est le premier levier pour accélérer un programme.

L'erreur classique

⚠️ Additionner au lieu de multiplier. Deux boucles de 55 tours emboîtées exécutent 2525 fois le corps intérieur, pas 1010 : on ajoute les tours de deux boucles successives, on multiplie ceux de deux boucles emboîtées.

⚠️ Compter chaque couple deux fois. Avec for j in range(n) au lieu de range(i + 1, n), la paire {2,5}\{2,5\} est vue comme (2,5)(2,5) et comme (5,2)(5,2), et les couples (i,i)(i,i) s'ajoutent : n2=100n^2=100 tours pour n=10n=10 au lieu de 4545. On impose i<ji<j par les bornes du range, plutôt que par un test qui laisserait la boucle parcourir les cas inutiles.

⚠️ Mal placer une initialisation. ligne = [] doit être entre les deux boucles pour repartir d'une ligne vide à chaque i ; au-dessus des deux boucles, on obtiendrait une seule liste qui s'allonge à chaque affichage, jusqu'à 2525 nombres.

Réponse. Table 5×55\times5 : cinq lignes, de [1, 2, 3, 4, 5] à [5, 10, 15, 20, 25], et append exécuté 5×5=255\times5=25 fois. Couples i<ji<j : ∑i(n−1−i)=n(n−1)2\sum_i(n-1-i)=\frac{n(n-1)}2, soit 4545 tours pour n=10n=10 (190190 pour 2020, 780780 pour 4040 : coût quadratique). Triplets a<b<c≤30a<b<c\leq30 : 1111 triplets pythagoriciens pour (303)=4 060\binom{30}3=4\,060 tests ; en calculant cc à partir de aa et bb, 406406 tests seulement.
Faire cet exercice dans l'app →

Indices de 0 à n−1 : lire, parcourir, modifier

DémonstrationDifficulté 2/5

On considère le programme suivant.

t = [7, 3, 9, 4, 6]
n = len(t)
print(t[0], t[n - 1], t[-1])

1. Qu'affiche-t-il ? Quels sont l'indice du premier élément et celui du dernier, pour une liste de longueur nn ?

2. Que se passe-t-il si l'on ajoute la ligne print(t[n]) ? Quels indices négatifs sont permis ?

3. On veut doubler chaque élément de t. Lequel des deux parcours for x in t: x = 2 * x et for i in range(n): t[i] = 2 * t[i] modifie réellement la liste ?

4. Écrire une boucle qui affiche chaque indice suivi de l'élément correspondant, puis calculer la somme des éléments d'indice pair.

Indices (3)

Une liste de longueur nn a nn cases : si la première porte le numéro 00, la dernière porte le numéro n−1n-1.

L'indice négatif −k-k compte à partir de la fin : t[-1] est le dernier élément. Cherche pour quels kk on reste dans la liste.

Dans for x in t, x est un nom qui désigne tour à tour chaque élément : le réaffecter change ce que désigne x, pas le contenu de la case.

Correction détaillée
Ce qu'il faut voir

Un indice n'est pas un rang, c'est un décalage depuis le début. Le premier élément est à distance 00 du début, le deuxième à distance 11… et le dernier d'une liste de longueur nn à distance n−1n-1. Les indices valides positifs ou nuls sont donc exactement 0,1,…,n−10,1,\dots,n-1 : il y en a nn, et nn lui-même n'en fait pas partie. Python accepte en plus les indices négatifs −1,…,−n-1,\dots,-n, qui comptent depuis la fin : t[-k] désigne t[n - k].

Deux façons de parcourir une liste en découlent. Par valeur (for x in t) : on lit les éléments un à un, sans savoir à quelle position on se trouve. Par indice (for i in range(len(t))) : on connaît la position i, et l'on peut écrire dans la case t[i]. Seul le second parcours donne directement la case à modifier.

👉 Retiens le couple « nn éléments, indices de 00 à n−1n-1 » : c'est lui qui explique à la fois pourquoi range(n) parcourt exactement les bonnes cases et pourquoi t[n] sort du tableau.

Premier élément, dernier élément, indices négatifs

On relance le programme de l'énoncé :

t = [7, 3, 9, 4, 6]
n = len(t)
print(t[0], t[n - 1], t[-1])
7 6 6

Ici n=5n=5 : le premier élément est t[0], qui vaut 7, et le dernier est t[4], qui vaut 6. L'écriture t[n - 1] désigne le dernier élément quelle que soit la longueur de la liste, ce qui la rend préférable à un t[4] écrit en dur. L'indice −1-1 désigne lui aussi le dernier élément : Python lit t[-k] comme t[n - k], donc t[-1] est t[4].

Vérification. On contrôle la règle t[-k] == t[n - k] pour tous les kk de 11 à nn par des assert, puis on lit deux indices négatifs :

t = [7, 3, 9, 4, 6]
n = len(t)
for k in range(1, n + 1):
    assert t[-k] == t[n - k]
print(t[-5], t[-2])
7 4

Les cinq assert passent sans rien dire — sinon le programme s'arrêterait sur une AssertionError —, t[-5] est bien t[0] et t[-2] l'avant-dernier élément.

Sortir du tableau

Ajoutons la ligne print(t[n]) après une lecture correcte du dernier élément :

t = [7, 3, 9, 4, 6]
n = len(t)
print(t[n - 1])
print(t[n])
6
IndexError: list index out of range

La première lecture affiche 6 normalement ; la seconde demande la case d'indice 5, qui n'existe pas dans une liste de 5 éléments. Python ne renvoie pas une valeur par défaut : il interrompt le programme sur une IndexError (« indice de liste hors limites »), et la dernière ligne de son message est celle qu'on lit ci-dessus.

Côté négatif, la règle t[-k] == t[n - k] n'a de sens que si n−k≥0n-k\geq0, c'est-à-dire k≤nk\leq n : les indices négatifs permis vont de −1-1 à −n-n, et t[-6] lèverait la même erreur. Au total, un indice kk est valide si et seulement si −n≤k≤n−1-n\leq k\leq n-1, soit 2n=102n=10 valeurs pour n=5n=5.

Vérification. Les deux bornes ont été éprouvées : t[-5] a fonctionné au bloc précédent (c'est la plus petite valeur permise), et t[5] échoue ici (c'est la première valeur interdite à droite).

Parcours par valeur contre parcours par indice

On essaie les deux boucles l'une après l'autre sur la même liste :

t = [7, 3, 9, 4, 6]
for x in t:
    x = 2 * x
print(t)
for i in range(len(t)):
    t[i] = 2 * t[i]
print(t)
print(sum(t))
[7, 3, 9, 4, 6]
[14, 6, 18, 8, 12]
58

La première boucle ne change rien : à chaque tour, x désigne un élément, puis x = 2 * x fabrique un nouveau nombre et y attache le nom x. La case de la liste n'a jamais été visée. La seconde boucle écrit dans la case t[i] elle-même : c'est une affectation dans la liste, et elle seule double les éléments.

Vérification. La somme de départ vaut 7+3+9+4+6=297+3+9+4+6=29 ; après la seconde boucle, sum(t) affiche 58=2×2958=2\times29, ce qui confirme que les cinq cases ont été doublées une fois, et une seule.

Afficher les positions, sommer les indices pairs

Le parcours par indice donne la position et la valeur à chaque tour. Pour la somme, on écrit deux versions :

t = [7, 3, 9, 4, 6]
n = len(t)
for i in range(n):
    print(i, t[i])
s = 0
for i in range(0, n, 2):
    s = s + t[i]
s2 = 0
for i in range(n):
    if i % 2 == 0:
        s2 = s2 + t[i]
print(s, s2)
0 7
1 3
2 9
3 4
4 6
22 22

La somme porte sur les indices 00, 22 et 44 : 7+9+6=227+9+6=22. La première version saute directement de deux en deux avec range(0, n, 2) (3 tours) ; la seconde parcourt tout et filtre par i % 2 == 0 (5 tours, dont 3 retenus).

Vérification. Les deux versions affichent 22. ⚠️ « Indice pair » n'est pas « élément pair » : les éléments pairs de t sont 4 et 6, de somme 10. On somme ici selon la position, pas selon la valeur.

Rappel de cours

Indices. Une liste t de longueur n = len(t) a pour indices 0,1,…,n−10,1,\dots,n-1. t[0] est le premier élément, t[n - 1] (ou t[-1]) le dernier. Un indice négatif −k-k, pour 1≤k≤n1\leq k\leq n, désigne t[n - k]. Tout autre indice lève IndexError.

Parcours. for x in t: lit les valeurs (réaffecter x ne modifie pas t) ; for i in range(len(t)): donne les positions et permet d'écrire t[i] = …. range(n) produit exactement les nn indices valides positifs ou nuls.

Modifier une case : t[i] = valeur. Réaffecter la variable de boucle x ne modifie pas la liste.

L'erreur classique

⚠️ Écrire t[len(t)] pour le dernier élément. C'est l'erreur du « un de trop » : la longueur compte les cases, le dernier indice vaut un de moins. Le bon réflexe : t[len(t) - 1] ou t[-1].

⚠️ Boucler sur range(1, n + 1) en pensant « de la première à la dernière case » : tu sautes t[0] et tu finis sur t[n], donc sur une IndexError. Les positions vont de 00 à n−1n-1 : range(n).

⚠️ Croire que for x in t: x = 2 * x double la liste. Aucun message ne prévient : le programme tourne, et la liste reste intacte. Pour modifier, passe par les indices.

Réponse. Le programme affiche 7 6 6 : les indices vont de 00 à n−1n-1, et t[-1] désigne t[n - 1]. t[n] lève IndexError: list index out of range ; les indices valides vont de −n-n à n−1n-1. Seul le parcours par indice modifie t, qui devient [14, 6, 18, 8, 12] (somme 58=2×2958=2\times29). Somme des éléments d'indice pair : 7+9+6=227+9+6=22.
Faire cet exercice dans l'app →

Maximum d'une liste et position du maximum

DémonstrationDifficulté 2/5

On cherche le plus grand élément d'une liste t non vide, et un indice où il est atteint.

t = [3, 8, 2, 8, 5]
m = t[0]
p = 0
for i in range(1, len(t)):
    if t[i] > m:
        m = t[i]
        p = i
print(m, p)

1. Dresser le tableau de trace des variables i, m et p, et donner l'affichage.

2. Un élève écrit m = 0 au lieu de m = t[0]. Qu'affiche alors son programme pour t = [-5, -2, -9, -3] ? Pourquoi m = t[0] est-il le bon choix ?

3. On remplace t[i] > m par t[i] >= m. Quelle occurrence du maximum chaque version repère-t-elle ?

4. Combien de comparaisons le programme effectue-t-il pour une liste de longueur nn ? Écrire une fonction position_max(t) qui renvoie l'indice du maximum.

Indices (3)

Pour la trace, écris une ligne « avant » (l'état après les deux affectations initiales), puis une ligne par tour de boucle : i prend les valeurs 1, 2, 3, 4.

Avec m = 0 et une liste de nombres négatifs, le test t[i] > m peut-il être vrai une seule fois ?

Avec >=, une égalité suffit à mettre p à jour : regarde le tour où t[i] vaut 8 pour la deuxième fois.

Correction détaillée
Ce qu'il faut voir

L'algorithme du maximum est un accumulateur qui garde « le meilleur vu jusqu'ici ». Après le tour d'indice ii, m est le plus grand des éléments t[0], …, t[i], et p un indice où il est atteint. Avant tout tour, le seul élément vu est t[0] : c'est donc la seule valeur légitime pour m. Chaque tour compare un nouvel élément au meilleur courant, et le remplace s'il fait mieux. À la fin, tout a été vu : m est le maximum de la liste.

👉 Deux décisions font toute la différence : l'initialisation (prendre un élément de la liste, jamais une valeur inventée comme 0) et l'inégalité (> garde la première occurrence, >= la dernière). Aucune des deux n'est un détail : chacune change l'affichage.

Le tableau de trace

On relance le programme et on suit les trois variables. La ligne « avant » donne l'état juste avant la boucle : i n'existe pas encore.

t = [3, 8, 2, 8, 5]
m = t[0]
p = 0
for i in range(1, len(t)):
    if t[i] > m:
        m = t[i]
        p = i
print(m, p)
8 1
Étape i m p
avant — 3 0
après le tour 1 1 8 1
après le tour 2 2 8 1
après le tour 3 3 8 1
après le tour 4 4 8 1

Au tour 1, t[1] vaut 8 et 8>38>3 : m et p changent ensemble. Aux tours 2 et 4, t[i] vaut 2 puis 5, plus petits que 8 : rien ne bouge. Le tour 3 est le seul délicat : t[3] vaut 8, et le test 8>88>8 est faux, donc p reste à 1. La boucle fait n−1=4n-1=4 tours, puisque range(1, 5) produit 1, 2, 3, 4.

Vérification. Le maximum se lit sur la liste : 8, présent aux indices 1 et 3. On a bien t[p] égal à m, et le programme a retenu la première des deux occurrences.

Initialiser à 0 : le piège des négatifs

Le programme de l'élève, sur une liste entièrement négative :

t = [-5, -2, -9, -3]
m = 0
p = 0
for i in range(1, len(t)):
    if t[i] > m:
        m = t[i]
        p = i
print(m, p)
0 0

Il annonce 0 comme maximum, alors que 0 n'est même pas dans la liste, et l'indice 0, qui désigne −5-5. Le test t[i] > m compare à chaque tour un nombre négatif à 0 : il est toujours faux, et les valeurs initiales survivent jusqu'au bout. Le défaut ne se voit pas seulement sur les listes entièrement négatives. Comme la boucle part de l'indice 11, t[0] n'est jamais comparé : dès que le maximum n'est atteint qu'en tête et qu'il est non nul, le programme le manque ([9, 1, 2] donne 2 2), et dès que les éléments d'indice ≥1\geq1 sont tous négatifs ou nuls, il répond 0 0 ([5, -1] donne 0 0). Sur [3, 8, 2, 8, 5], le maximum est positif et n'est pas en tête : m = 0 donne la bonne réponse, ce qui rend l'erreur invisible sur un essai « ordinaire ».

Avec m = t[0], la valeur de départ appartient à la liste, donc le résultat aussi, quels que soient les signes :

t = [-5, -2, -9, -3]
m = t[0]
p = 0
for i in range(1, len(t)):
    if t[i] > m:
        m = t[i]
        p = i
print(m, p)
print(max(t))
-2 1
-2

Vérification. La fonction max de Python donne le même −2-2, et t[1] vaut bien −2-2. C'est aussi pourquoi l'énoncé précise « non vide » : sur [], la case t[0] n'existe pas.

Première ou dernière occurrence

On remplace > par >= :

t = [3, 8, 2, 8, 5]
m = t[0]
p = 0
for i in range(1, len(t)):
    if t[i] >= m:
        m = t[i]
        p = i
print(m, p)
8 3
Étape i m p
avant — 3 0
après le tour 1 1 8 1
après le tour 2 2 8 1
après le tour 3 3 8 3
après le tour 4 4 8 3

Seul le tour 3 diffère : 8≥88\geq8 est vrai, donc p passe à 3. Avec >, un élément égal au maximum courant ne le détrône pas : on garde la première occurrence. Avec >=, il le remplace : on garde la dernière. Le maximum m est le même dans les deux versions ; seule la position change, et seulement quand le maximum apparaît plusieurs fois.

Vérification. Sur une liste dont le maximum est unique, les deux versions doivent s'accorder. On les fait tourner côte à côte :

t = [3, 9, 2]
p1 = 0
p2 = 0
for i in range(1, len(t)):
    if t[i] > t[p1]:
        p1 = i
    if t[i] >= t[p2]:
        p2 = i
print(p1, p2)
1 1

Même indice : la différence ne se voit qu'avec des ex æquo, d'où l'intérêt d'un jeu de test qui en contienne.

Le nombre de comparaisons, puis une fonction

La boucle parcourt les indices 11 à n−1n-1 et fait une comparaison par tour : n−1n-1 comparaisons, quelle que soit la liste. On ne peut pas faire mieux avec des comparaisons entre éléments : chacune des n−1n-1 positions qui ne sont pas retenues doit avoir « perdu » au moins une comparaison (sinon rien ne l'exclut), et une comparaison n'a qu'un perdant. On le mesure avec un compteur, sur une liste de 7 éléments :

t = [4, 1, 7, 7, 2, 9, 3]
c = 0
p = 0
for i in range(1, len(t)):
    c = c + 1
    if t[i] > t[p]:
        p = i
print(len(t), c, p)
7 6 5

On lit n=7n=7, puis 6=n−16=n-1 comparaisons, et le maximum 9 à l'indice 5. Écrite comme fonction, la recherche n'a plus besoin de m : t[p] est le maximum courant, et la fonction renvoie sa position.

def position_max(t):
    p = 0
    for i in range(1, len(t)):
        if t[i] > t[p]:
            p = i
    return p

t = [3, 8, 2, 8, 5]
p = position_max(t)
print(p, t[p])
print(position_max([-5, -2, -9]))
print(position_max([4]))
1 8
1
0

Vérification. La première ligne redonne l'indice 1 et la valeur 8 du tableau de trace ; la liste de négatifs donne l'indice 1, celui de −2-2 ; et une liste d'un seul élément renvoie 0 sans faire aucun tour, puisque range(1, 1) est vide.

Rappel de cours

Maximum d'une liste non vide. Initialiser le maximum courant avec t[0] et sa position avec 0, puis parcourir les indices de 1 à len(t) - 1 en remplaçant le maximum courant dès qu'un élément le dépasse. Coût : n−1n-1 comparaisons.

Occurrence retenue. Avec >, on garde la première position du maximum ; avec >=, la dernière.

Minimum. Même algorithme en renversant l'inégalité.

L'erreur classique

⚠️ Initialiser avec 0, ou avec une « très petite » valeur choisie au hasard comme −1000-1000. Le programme marche sur les listes que tu as en tête et échoue sur les autres : une valeur de départ étrangère à la liste, 00 ou −1000-1000, n'est jamais comparée à t[0], et le résultat est faux exactement quand t[0] ne vaut pas cette valeur et que, soit le maximum n'est atteint qu'en tête, soit les autres éléments sont tous inférieurs ou égaux à cette valeur. Aucune constante finie ne convient à toutes les listes : on initialise avec un élément de la liste, et avec m = t[0], partir de l'indice 11 est correct.

⚠️ Oublier de mettre à jour p en même temps que m, ou placer p = i hors du if : p finit alors sur le dernier indice parcouru, pas sur celui du maximum. Les deux affectations vont ensemble, dans le même bloc.

Réponse. Trace : m passe de 3 à 8 au tour 1, puis ne bouge plus ; affichage 8 1. Avec m = 0, la liste [-5, -2, -9, -3] donne 0 0, un résultat faux ; avec m = t[0], on obtient -2 1. > garde la première occurrence du maximum (indice 1), >= la dernière (indice 3). Coût : n−1n-1 comparaisons ; position_max renvoie l'indice, ici 1.
Faire cet exercice dans l'app →

Recherche séquentielle et comptage

DémonstrationDifficulté 2/5

On travaille sur la liste t = [4, 7, 1, 7, 9, 7].

1. Écrire une fonction indice(t, x) qui renvoie le premier indice où x apparaît dans t, et -1 si x n'y est pas. La tester avec x = 7, x = 5 et x = 4.

2. Combien de comparaisons t[i] == x sont effectuées dans chacun de ces cas ? Et au pire, pour une liste de longueur nn ?

3. Un élève propose la version suivante. Que renvoie-t-elle pour x = 7 ? Où est l'erreur ?

def indice(t, x):
    for i in range(len(t)):
        if t[i] == x:
            return i
        else:
            return -1

4. Écrire une fonction compte(t, x) qui renvoie le nombre d'occurrences de x. Pourquoi ne peut-elle pas s'arrêter à la première occurrence trouvée ?

Indices (3)

Un return placé dans la boucle arrête la fonction immédiatement : c'est exactement la « sortie anticipée » voulue quand on trouve x.

Le return -1 ne doit être atteint qu'une fois toutes les cases examinées : où faut-il donc le placer par rapport à la boucle ?

Pour compter les comparaisons, ajoute un compteur c augmenté de 1 juste avant chaque test t[i] == x.

Correction détaillée
Ce qu'il faut voir

Chercher, c'est parcourir jusqu'à trouver ; compter, c'est parcourir jusqu'au bout. La recherche séquentielle examine les cases dans l'ordre et s'arrête dès qu'elle trouve : l'instruction return sort de la fonction au milieu de la boucle, et c'est voulu. Elle ne peut conclure « absent » qu'après avoir tout examiné : le return -1 se place donc après la boucle, pas dans un else. Le comptage, lui, ne peut jamais s'arrêter en route, puisqu'une occurrence plus loin changerait le résultat.

👉 Retiens l'emplacement des deux return : l'un dans la boucle (« trouvé »), l'autre après (« pas trouvé »). Ce schéma revient dans toute fonction qui cherche un témoin : premier diviseur d'un entier, premier terme d'une suite qui dépasse un seuil…

La fonction et ses tests

On écrit la fonction et on la teste, y compris sur une liste vide :

def indice(t, x):
    for i in range(len(t)):
        if t[i] == x:
            return i
    return -1

t = [4, 7, 1, 7, 9, 7]
print(indice(t, 7))
print(indice(t, 5))
print(indice(t, 4))
print(indice([], 7))
1
-1
0
-1

Pour x = 7, la boucle s'arrête à i = 1 : les occurrences suivantes, aux indices 3 et 5, ne sont jamais regardées. Pour x = 5, aucune case ne convient, la boucle se termine normalement et l'on atteint return -1. Pour x = 4, l'élément est en tête : réponse 0, qui est un indice valide — d'où le choix de −1-1, et non de 0, pour signaler l'absence. Sur la liste vide, la boucle ne fait aucun tour et la fonction renvoie −1-1, ce qui est juste.

Vérification. On relit t : t[0] vaut 4, différent de 7, et t[1] vaut 7, donc 1 est bien le premier indice de 7 ; quant à 5, il n'apparaît dans aucune des six cases.

Compter les comparaisons

On ajoute un compteur, et la fonction renvoie deux valeurs, récupérées par r, c = … :

def indice_c(t, x):
    c = 0
    for i in range(len(t)):
        c = c + 1
        if t[i] == x:
            return i, c
    return -1, c

t = [4, 7, 1, 7, 9, 7]
for x in [7, 5, 4, 9]:
    r, c = indice_c(t, x)
    print(x, r, c)
7 1 2
5 -1 6
4 0 1
9 4 5

On lit une règle simple : si x est trouvé à l'indice ii, il a fallu i+1i+1 comparaisons (celles des cases 00 à ii) ; s'il est absent, il en faut n=6n=6, une par case. Le pire cas est donc nn comparaisons, atteint quand x est absent ou seulement présent en dernière position ; le meilleur, 1 comparaison, quand x est en tête.

Vérification. Les quatre lignes respectent la règle : 1+1=21+1=2 pour 7, 6=n6=n pour l'absent 5, 0+1=10+1=1 pour 4, 4+1=54+1=5 pour 9.

La version fautive

On fait tourner la version de l'élève :

def indice(t, x):
    for i in range(len(t)):
        if t[i] == x:
            return i
        else:
            return -1

t = [4, 7, 1, 7, 9, 7]
print(indice(t, 7))
print(indice([], 7))
-1
None

Elle renvoie −1-1 pour x = 7, qui est pourtant dans la liste. Au premier tour, t[0] vaut 4 : le test échoue, la branche else s'exécute, et return -1 termine la fonction dès le premier tour. Cette version ne regarde donc jamais que t[0]. Deuxième défaut, plus sournois : sur une liste vide, la boucle ne fait aucun tour, aucun return n'est atteint, et la fonction renvoie None — ni un indice, ni −1-1.

La correction consiste à supprimer le else et à placer return -1 après la boucle.

Vérification. La version corrigée, celle du bloc « La fonction et ses tests », renvoie bien 1 pour 7 et −1-1 pour la liste vide.

Compter toutes les occurrences

Pour compter, aucune sortie anticipée : on parcourt tout, et l'on renvoie le compteur après la boucle.

def compte(t, x):
    c = 0
    for y in t:
        if y == x:
            c = c + 1
    return c

t = [4, 7, 1, 7, 9, 7]
print(compte(t, 7), compte(t, 5))
s = 0
for x in [4, 7, 1, 9]:
    s = s + compte(t, x)
print(s, len(t))
3 0
6 6

compte ne contient aucun return dans la boucle : elle examine les nn cases à chaque appel, parce qu'une occurrence peut toujours se trouver plus loin. Ici 7 apparaît 3 fois (indices 1, 3 et 5) et 5 jamais. Le parcours par valeur suffit, puisque la position ne sert pas.

Vérification. Chaque case contient exactement une valeur : la somme des nombres d'occurrences des valeurs distinctes 4, 7, 1, 9 doit redonner la longueur de la liste. La dernière ligne affiche 6 et 6.

Rappel de cours

Recherche séquentielle. Parcourir t ; dès que t[i] == x, renvoyer i. Après la boucle, renvoyer une valeur conventionnelle d'absence, ici −1-1. Coût : de 1 à nn comparaisons, nn quand x est absent.

Comptage. Un compteur initialisé à 0 avant la boucle, augmenté de 1 à chaque occurrence ; aucune sortie anticipée n'est possible : toujours nn comparaisons.

return termine l'appel immédiatement, même au milieu d'une boucle. Une fonction qui se termine sans rencontrer de return renvoie None.

L'erreur classique

⚠️ Le return -1 dans un else : la fonction décide « absent » dès la première case différente de x. Un test sur un élément placé en tête passe quand même, ce qui masque l'erreur : teste toujours un élément placé ailleurs qu'en première position.

⚠️ Utiliser le résultat comme indice sans le contrôler. −1-1 est un indice valide en Python : t[-1] est le dernier élément. Le programme suivant ne lève aucune erreur, et affiche un élément de la liste pour une valeur qui n'y figure pas :

def indice(t, x):
    for i in range(len(t)):
        if t[i] == x:
            return i
    return -1

t = [4, 7, 1, 7, 9, 5]
print(t[indice(t, 8)])
5

8 est absent, indice renvoie −1-1, et t[-1] vaut 5. Avant d'écrire t[r], teste if r != -1:.

Réponse. indice renvoie 1, −1-1 et 0 pour 7, 5 et 4 : trouvé à l'indice ii, il a fallu i+1i+1 comparaisons ; absent, n=6n=6, le pire cas. La version avec else: return -1 s'arrête au premier tour (−1-1 pour 7) et renvoie None sur une liste vide. compte(t, 7) vaut 3 et parcourt toujours les nn cases ; la somme des comptes des valeurs distinctes redonne n=6n=6.
Faire cet exercice dans l'app →

Construire une liste : carrés, filtre, moyenne et variance

DémonstrationDifficulté 3/5

Les notes d'un groupe de six étudiants sont rangées dans la liste notes = [9, 14, 11, 8, 15, 15].

1. Construire, avec une boucle et append, la liste des carrés des notes, puis la liste des notes supérieures ou égales à 10.

2. Écrire une fonction moyenne(t) qui calcule la moyenne d'une liste non vide sans utiliser sum, et l'appliquer à notes.

3. Écrire une fonction variance(t) qui calcule la variance V=1n∑i(xi−xˉ)2V=\frac1n\sum_{i}(x_i-\bar x)^2. Donner la variance et l'écart-type des notes, arrondi au centième.

4. Recouper la variance par la formule de Koenig V=x2‾−xˉ2V=\overline{x^2}-\bar x^2, en calculant la moyenne des carrés à la main.

Indices (3)

Une liste se construit comme une somme : on part de la liste vide [] et on ajoute un élément par tour avec append.

La variance demande deux passages : d'abord la moyenne xˉ\bar x, puis la somme des carrés des écarts. variance peut appeler moyenne.

Pour Koenig, la liste des carrés de la question 1 donne directement x2‾\overline{x^2} : divise sa somme par 6.

Correction détaillée
Ce qu'il faut voir

Une liste se construit comme une somme se calcule. Pour une somme, on part de s = 0 et on ajoute un terme par tour ; pour une liste, on part de t = [] et on ajoute un élément par tour avec t.append(…). Filtrer, c'est n'ajouter que sous condition. La moyenne et la variance sont ensuite deux accumulateurs numériques : la moyenne se calcule en un parcours ; la variance en demande deux par sa définition, puisqu'il faut connaître xˉ\bar x avant de mesurer les écarts, et un seul par la formule de Koenig, qui accumule ∑xi\sum x_i et ∑xi2\sum x_i^2 en même temps.

👉 Retiens la division par nn, c'est-à-dire par len(t) : c'est la variance de la série statistique, celle du chapitre de statistique descriptive. Et vérifie toujours une variance par un second chemin, la formule de Koenig, qui ne demande que la moyenne des carrés.

Carrés et filtre

Les deux listes se remplissent dans la même boucle :

notes = [9, 14, 11, 8, 15, 15]
carres = []
bonnes = []
for x in notes:
    carres.append(x * x)
    if x >= 10:
        bonnes.append(x)
print(carres)
print(bonnes)
print(len(carres), len(bonnes))
[81, 196, 121, 64, 225, 225]
[14, 11, 15, 15]
6 4

carres reçoit un élément à chaque tour, bonnes seulement quand le test x >= 10 est vrai. L'ordre d'origine est conservé, puisque append ajoute toujours en fin de liste.

Vérification. carres a autant d'éléments que notes, soit 6 ; bonnes en a 4, et les deux notes écartées, 9 et 8, complètent bien le compte : 4+2=64+2=6.

La moyenne

On accumule la somme dans s, puis on divise par la longueur :

def moyenne(t):
    s = 0
    for x in t:
        s = s + x
    return s / len(t)

notes = [9, 14, 11, 8, 15, 15]
print(moyenne(notes))
print(sum(notes) / len(notes))
print(moyenne([10]))
12.0
12.0
10.0

La somme vaut 9+14+11+8+15+15=729+14+11+8+15+15=72, et 72/6=1272/6=12. Python affiche 12.0 et non 12 : l'opérateur / renvoie toujours un flottant, même quand la division tombe juste. L'opérateur // donnerait l'entier 12 ici, mais il arrondirait à l'entier inférieur une moyenne non entière comme 12,5 : ce n'est pas le bon outil.

Vérification. Le calcul direct sum(notes) / len(notes) redonne 12.0, et la moyenne d'une liste à un seul élément est bien cet élément.

La variance en divisant par n

La fonction variance calcule d'abord la moyenne, puis accumule les carrés des écarts :

import math

def moyenne(t):
    s = 0
    for x in t:
        s = s + x
    return s / len(t)

def variance(t):
    m = moyenne(t)
    s = 0
    for x in t:
        s = s + (x - m) ** 2
    return s / len(t)

notes = [9, 14, 11, 8, 15, 15]
v = variance(notes)
print(v)
print(round(math.sqrt(v), 2))
print(variance([12, 12, 12]))
8.0
2.83
0.0

Les écarts à la moyenne sont −3, 2, −1, −4, 3, 3-3,\ 2,\ -1,\ -4,\ 3,\ 3, de somme nulle comme il se doit ; leurs carrés donnent

9+4+1+16+9+9=48,V=486=8,σ=8=22≈2,83.9+4+1+16+9+9=48,\qquad V=\frac{48}{6}=8,\qquad \sigma=\sqrt8=2\sqrt2\approx2{,}83.

Le programme affiche 8.0 puis 2.83, l'écart-type étant arrondi au centième par round(…, 2). On remarque que variance appelle moyenne une seule fois, avant la boucle : la placer dans la boucle referait le même calcul à chaque tour.

Vérification. Une série constante n'a aucune dispersion : variance([12, 12, 12]) affiche bien 0.0.

Le recoupement par Koenig

La liste des carrés de la question 1 donne la moyenne des carrés :

x2‾=81+196+121+64+225+2256=9126=152.\overline{x^2}=\frac{81+196+121+64+225+225}{6}=\frac{912}{6}=152.

La formule de Koenig donne alors V=x2‾−xˉ2=152−122=152−144=8V=\overline{x^2}-\bar x^2=152-12^2=152-144=8 : la même valeur, obtenue sans calculer un seul écart. Le programme peut le faire en un seul passage, avec deux accumulateurs :

notes = [9, 14, 11, 8, 15, 15]
n = len(notes)
s1 = 0
s2 = 0
for x in notes:
    s1 = s1 + x
    s2 = s2 + x * x
m = s1 / n
print(s2, s2 / n, m * m)
print(s2 / n - m * m)
912 152.0 144.0
8.0

Vérification. Les deux chemins, par les écarts et par Koenig, donnent 8. Si l'on avait divisé la somme des carrés des écarts par n−1=5n-1=5 — la convention de certaines calculatrices, discutée dans le chapitre de statistique descriptive —, on aurait obtenu 48/5=9,648/5=9{,}6 : une autre quantité, qui n'est pas la variance de la série.

Rappel de cours

Construire une liste. t = [] avant la boucle, puis t.append(v) à chaque tour : ajout en fin de liste. Filtrer : if condition: puis t.append(v).

Moyenne. xˉ=1n∑xi\bar x=\frac1n\sum x_i ; en Python, s / len(t) après une boucle d'accumulation. Le résultat de / est un flottant.

Variance (en 1n\frac1n). V=1n∑(xi−xˉ)2=x2‾−xˉ2V=\frac1n\sum(x_i-\bar x)^2=\overline{x^2}-\bar x^2 (Koenig) ; écart-type σ=V\sigma=\sqrt V.

L'erreur classique

⚠️ Confondre la moyenne des carrés et le carré de la moyenne. Ici x2‾=152\overline{x^2}=152 et xˉ2=144\bar x^2=144 : ce sont deux nombres différents, et leur différence est la variance. Écrire s2 / n à la place de s2 / n - m * m donnerait 152 au lieu de 8.

⚠️ Appeler moyenne sur une liste vide. len(t) vaut 0, et la division échoue :

def moyenne(t):
    s = 0
    for x in t:
        s = s + x
    return s / len(t)

print(moyenne([]))
ZeroDivisionError: division by zero

Une moyenne n'a de sens que sur une série non vide : tu peux le rendre explicite par assert len(t) > 0 en tête de fonction, qui arrête le programme dès l'entrée, sur la ligne de la condition violée, plutôt qu'au milieu d'un calcul.

Réponse. Carrés : [81, 196, 121, 64, 225, 225] ; notes supérieures ou égales à 10 : [14, 11, 15, 15]. Moyenne 12 (Python affiche 12.0, car / renvoie un flottant). Variance V=48/6=8V=48/6=8, écart-type 8≈2,83\sqrt8\approx2{,}83. Koenig : x2‾=912/6=152\overline{x^2}=912/6=152 et 152−122=8152-12^2=8, même résultat. La moyenne d'une liste vide lève une ZeroDivisionError.
Faire cet exercice dans l'app →

Fonctions : renvoyer n'est pas afficher, et un test de primalité

DémonstrationDifficulté 3/5

1. Qu'affiche le programme suivant ? Pourquoi sa dernière ligne n'affiche-t-elle pas 9 ?

def carre(x):
    print(x * x)

y = carre(3)
print(y)

2. Une fonction double calcule y = 2 * x, puis renvoie y. On exécute l'instruction double(5) seule, sans ranger son résultat, puis print(y). Que se passe-t-il ?

3. Écrire une fonction booléenne est_premier(n) qui cherche un diviseur d à partir de 2 tant que d * d <= n. Justifier que cette borne suffit, et expliquer pourquoi <= ne peut pas être remplacé par <.

4. En déduire la liste des nombres premiers inférieurs à 30, puis le nombre de divisions effectuées pour tester n=97n=97.

Indices (3)

print écrit à l'écran ; return renvoie une valeur à l'appelant. Que renvoie une fonction qui ne contient aucun return ?

Si n=abn=ab avec 2≤a≤b2\leq a\leq b, compare a2a^2 et nn.

Pour le <=, essaie n=9n=9 : quel est son seul diviseur entre 2 et 8, et que vaut son carré ?

Correction détaillée
Ce qu'il faut voir

Une fonction renvoie une valeur ; elle n'affiche rien, sauf si on le lui demande. return v termine l'appel et remplace l'expression f(…) par v, qu'on peut ranger dans une variable, additionner, tester. print(v) écrit v à l'écran pour un lecteur humain, et c'est tout : le programme, lui, ne récupère rien. Une fonction sans return renvoie la valeur spéciale None. Enfin, les variables créées dans une fonction sont locales : elles naissent à l'appel et disparaissent à la fin de l'appel.

👉 Retiens le critère : si le résultat doit servir — dans un calcul, dans un if, dans une liste —, il faut return. Une fonction booléenne comme est_premier en est l'exemple type : elle renvoie True ou False, et c'est un if qui s'en sert.

Afficher au lieu de renvoyer

On lance le programme de l'énoncé :

def carre(x):
    print(x * x)

y = carre(3)
print(y)
9
None

L'appel carre(3) exécute le corps de la fonction, qui affiche 9 : c'est la première ligne. Puis la fonction se termine sans rencontrer de return, et renvoie donc None : c'est cette valeur que reçoit y, et la seconde ligne affiche None. Le 9 est passé à l'écran, pas dans le programme. Toute utilisation de y dans un calcul, comme y + 1, provoquerait une TypeError.

La version correcte renvoie la valeur et laisse l'appelant décider de l'afficher :

def carre(x):
    return x * x

y = carre(3)
print(y, y + 1)
print(carre(carre(2)))
9 10
16

Vérification. y + 1 vaut bien 10, et l'on peut même composer : carre(carre(2))=carre(4)=16\text{carre}(\text{carre}(2))=\text{carre}(4)=16, ce qui est impossible avec la version qui affiche.

Variables locales

On appelle double(5) sans ranger le résultat, puis on lit y :

def double(x):
    y = 2 * x
    return y

double(5)
print(y)
NameError: name 'y' is not defined

L'appel crée bien un y valant 10, mais dans la fonction ; à la sortie, ce y disparaît, et la valeur renvoyée n'a été rangée nulle part puisque l'appel n'est pas affecté. Au niveau principal, aucun y n'existe : NameError, « le nom y n'est pas défini ». La bonne écriture récupère la valeur : y = double(5).

Un paramètre est lui aussi local : le réaffecter dans la fonction (x = 2 * x) ne change pas la variable de l'appelant, même si elle porte le même nom.

def double(x):
    x = 2 * x
    return x

x = 5
print(double(x), x)
10 5

Vérification. La fonction a renvoyé 10, et le x principal vaut toujours 5 : ce sont deux variables distinctes qui portent le même nom.

En revanche, une liste reçue en paramètre et modifiée sur place l'est aussi pour l'appelant, car le paramètre désigne la même liste (exercice E6) :

def f(t):
    t[0] = 2 * t[0]

u = [5, 6]
f(u)
print(u)
[10, 6]
Pourquoi s'arrêter quand d dépasse la racine

La borne. Soit n≥2n\geq2 un entier non premier : n=abn=ab avec 2≤a≤b2\leq a\leq b. Alors a2≤ab=na^2\leq ab=n. Tout entier composé possède donc un diviseur dd tel que 2≤d2\leq d et d2≤nd^2\leq n. Par contraposée : si aucun d≥2d\geq2 tel que d2≤nd^2\leq n ne divise nn, alors nn est premier. Tester les dd tels que d2>nd^2>n est inutile.

def est_premier(n):
    if n < 2:
        return False
    d = 2
    while d * d <= n:
        if n % d == 0:
            return False
        d = d + 1
    return True

for n in [1, 2, 9, 97]:
    print(n, est_premier(n))
1 False
2 True
9 False
97 True

1 n'est pas premier, cas traité à part par n < 2 ; pour 2, la boucle ne fait aucun tour puisque 22=4>22^2=4>2 ; 9 est composé ; 97 est premier. Le test d * d <= n reste dans les entiers : pas de math.sqrt, donc aucun arrondi de flottant à craindre.

Le <= est indispensable. Quand nn est le carré d'un nombre premier, son seul diviseur non trivial vérifie exactement d2=nd^2=n. Avec <, il n'est jamais testé :

def est_premier(n):
    if n < 2:
        return False
    d = 2
    while d * d < n:
        if n % d == 0:
            return False
        d = d + 1
    return True

for n in [9, 25, 49]:
    print(n, est_premier(n))
9 True
25 True
49 True

9, 25 et 49 sont déclarés premiers à tort, comme tout carré d'un nombre premier, à commencer par 4.

Vérification. On compare est_premier à une version lente, qui essaie tous les dd de 2 à n−1n-1, sur les mille premiers entiers :

def lent(n):
    if n < 2:
        return False
    for d in range(2, n):
        if n % d == 0:
            return False
    return True

def est_premier(n):
    if n < 2:
        return False
    d = 2
    while d * d <= n:
        if n % d == 0:
            return False
        d = d + 1
    return True

ok = True
for n in range(1000):
    if lent(n) != est_premier(n):
        ok = False
print(ok)
True

Les deux versions s'accordent sur 0,1,…,9990,1,\dots,999.

Les premiers jusqu'à 30, et le coût d'un test

On construit la liste avec append, comme une accumulation filtrée :

def est_premier(n):
    if n < 2:
        return False
    d = 2
    while d * d <= n:
        if n % d == 0:
            return False
        d = d + 1
    return True

p = []
for n in range(30):
    if est_premier(n):
        p.append(n)
print(p)
print(len(p))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
10

Pour n=97n=97, qui est premier, la boucle va jusqu'au bout. On compte ses tours :

n = 97
c = 0
d = 2
while d * d <= n:
    c = c + 1
    d = d + 1
print(c, d)
8 10

Elle teste d=2,3,…,9d=2,3,\dots,9, soit 8 divisions, et s'arrête quand d=10d=10, car 102=100>9710^2=100>97. La version lente en ferait 97−2=9597-2=95. Pour un nombre premier nn, on passe ainsi d'environ nn divisions à environ n\sqrt n.

Vérification. 92=81≤97<100=1029^2=81\leq97<100=10^2 : le dernier diviseur essayé est 9, et le compteur vaut 9−2+1=89-2+1=8.

Rappel de cours

return contre print. return v renvoie v à l'appelant et termine l'appel ; print(v) affiche v et ne renvoie rien. Sans return, une fonction renvoie None.

Portée. Les paramètres et les variables affectées dans une fonction sont locaux : invisibles hors de l'appel, distincts des variables de même nom du programme principal.

Primalité par essais de divisions. Un entier n≥2n\geq2 est premier si et seulement si aucun entier d≥2d\geq2 tel que d2≤nd^2\leq n ne le divise. En Python : while d * d <= n.

L'erreur classique

⚠️ Mettre un print là où il faut un return. À l'écran, tout semble juste — le bon nombre s'affiche —, mais la valeur renvoyée est None, et l'erreur n'éclate que plus loin, quand on s'en sert. Une fonction qui calcule doit renvoyer ; c'est l'appelant qui décide d'afficher.

⚠️ Écrire return True dans la boucle de est_premier, dès qu'un d ne divise pas nn : la fonction conclurait « premier » après un seul essai. « Premier » ne se conclut qu'après toutes les divisions, donc après la boucle : c'est le schéma de la recherche séquentielle.

⚠️ Oublier le cas n<2n<2. Sans lui, 0 et 1 passent pour premiers, puisque la boucle ne fait aucun tour :

def est_premier(n):
    d = 2
    while d * d <= n:
        if n % d == 0:
            return False
        d = d + 1
    return True

for n in [0, 1]:
    print(n, est_premier(n))
0 True
1 True
Réponse. Le programme affiche 9 puis None : carre affiche au lieu de renvoyer, et sans return une fonction renvoie None. Après double(5) non affecté, print(y) lève NameError : y était local. est_premier teste les dd tels que d2≤nd^2\leq n, car un composé n=abn=ab avec a≤ba\leq b vérifie a2≤na^2\leq n ; avec <, les carrés de nombres premiers, comme 4, 9, 25 et 49, passeraient pour premiers. Premiers inférieurs à 30 : 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 ; tester 97 coûte 8 divisions.
Faire cet exercice dans l'app →

Aliasing : deux noms pour une même liste

DémonstrationDifficulté 3/5

1. Qu'affiche le programme suivant ? Expliquer.

a = [1, 2, 3]
b = a
b[0] = 99
print(a)

2. Comment obtenir une liste b indépendante de a, qu'on puisse modifier sans toucher à a ?

3. On définit def f(t): t.append(0) et def g(n): n = n + 1. Après u = [5], k = 5, f(u) et g(k), que valent u et k ?

4. Dans une fonction qui reçoit une liste t, comparer t.append(x) et t = t + [x] : lequel modifie la liste de l'appelant ?

Indices (3)

Une affectation b = a ne fabrique aucune liste : elle donne un second nom à la liste qui existe déjà.

list(a) fabrique une nouvelle liste qui contient les mêmes éléments que a.

Distingue modifier un objet (t.append(…), t[i] = …) et réaffecter un nom (t = …, n = …) : seul le premier se voit de l'extérieur.

Correction détaillée
Ce qu'il faut voir

En Python, une variable est une étiquette posée sur un objet, pas une boîte qui le contient. a = [1, 2, 3] fabrique une liste et y colle l'étiquette a ; b = a colle une seconde étiquette sur la même liste. Il n'y a qu'une liste : la modifier par b se voit par a. C'est l'aliasing, deux noms pour un même objet.

Deux gestes sont à distinguer. Modifier l'objet (b[0] = 99, t.append(x)) : tous les noms qui le désignent voient le changement. Réaffecter un nom (t = t + [x], n = n + 1) : on fabrique un nouvel objet et on y déplace une étiquette ; les autres noms continuent de désigner l'ancien objet.

👉 Retiens : une liste passée à une fonction est partagée avec l'appelant, et la fonction peut la modifier ; un entier ne se modifie pas, il se remplace, donc une fonction ne peut pas changer l'entier de l'appelant.

Deux noms, une seule liste

On lance le programme de l'énoncé, en ajoutant un test d'identité :

a = [1, 2, 3]
b = a
b[0] = 99
print(a)
print(a is b)
[99, 2, 3]
True

b[0] = 99 modifie la liste désignée par b, qui est aussi celle désignée par a : a affiche [99, 2, 3]. Pour qui croyait avoir fait une copie, le programme est fautif, et pourtant il ne lève aucune erreur : rien ne signale l'aliasing.

Vérification. L'opérateur is teste si deux noms désignent le même objet ; a is b vaut True : il n'y a bien qu'une liste en mémoire.

La copie par list

list(a) construit une nouvelle liste contenant les mêmes éléments :

a = [1, 2, 3]
b = list(a)
print(a == b, a is b)
b[0] = 99
print(a, b)
True False
[1, 2, 3] [99, 2, 3]

Juste après la copie, a == b est vrai — mêmes valeurs, dans le même ordre — mais a is b est faux : ce sont deux objets distincts. Modifier b laisse donc a intact.

Vérification. La première ligne sépare les deux tests : == compare les contenus, is compare les identités. La seconde montre a inchangé à côté de b modifiée.

Une liste contre un entier dans une fonction

On fait tourner les deux fonctions de l'énoncé :

def f(t):
    t.append(0)

def g(n):
    n = n + 1

u = [5]
k = 5
f(u)
g(k)
print(u, k)
[5, 0] 5

À l'appel f(u), le paramètre t devient une étiquette de plus sur la liste de u : t.append(0) modifie cette liste, et u voit [5, 0]. À l'appel g(k), le paramètre n désigne l'entier 5 ; n = n + 1 fabrique l'entier 6 et y déplace l'étiquette locale n, sans toucher à k, qui vaut toujours 5. Un entier ne se modifie jamais sur place : il n'existe pas d'équivalent de append pour lui. Pour qu'une fonction « change » un entier, elle doit renvoyer la nouvelle valeur, que l'appelant réaffecte.

Vérification. L'affichage [5, 0] 5 montre les deux comportements côte à côte : la liste a changé, l'entier non.

Ajouter en place ou fabriquer une nouvelle liste

On compare les deux écritures dans une fonction :

def ajoute1(t, x):
    t.append(x)

def ajoute2(t, x):
    t = t + [x]

a = [1, 2]
ajoute1(a, 3)
print(a)
ajoute2(a, 4)
print(a)
[1, 2, 3]
[1, 2, 3]

ajoute1 modifie la liste reçue : a devient [1, 2, 3]. ajoute2 calcule t + [x], qui est une nouvelle liste [1, 2, 3, 4], et y déplace l'étiquette locale t ; la liste de l'appelant n'est pas touchée, et la nouvelle liste est perdue à la fin de l'appel. a reste [1, 2, 3].

Le même contraste se voit hors de toute fonction, dès qu'une liste a deux noms :

a = [1, 2]
b = a
a = a + [3]
print(a, b)
c = [1, 2]
d = c
c.append(3)
print(c, d)
[1, 2, 3] [1, 2]
[1, 2, 3] [1, 2, 3]

a = a + [3] a fabriqué une nouvelle liste pour a seul ; c.append(3) a modifié la liste que c et d partagent. Autre différence : t + [x] recopie tous les éléments de t dans une nouvelle liste, alors que append ajoute en place, en temps constant en moyenne : Python réserve de la place d'avance et ne recopie la liste que de loin en loin.

Si l'on tient à t + [x] dans une fonction, il faut renvoyer la nouvelle liste et la récupérer :

def ajoute2(t, x):
    t = t + [x]
    return t

a = [1, 2]
a = ajoute2(a, 4)
print(a)
[1, 2, 4]

Vérification. Cette fois a contient bien 4 : la nouvelle liste a été renvoyée, puis l'étiquette a y a été déplacée par l'appelant lui-même.

Rappel de cours

Affectation d'une liste. b = a ne copie pas : a et b désignent la même liste, et a is b vaut True. Copie : b = list(a), une nouvelle liste (indépendante tant qu'elle contient des nombres).

Modifier ou réaffecter. t[i] = v et t.append(v) modifient la liste, et tous ses noms le voient ; t = … réaffecte seulement le nom t.

Passage à une fonction. Le paramètre désigne l'objet de l'appelant : une liste peut être modifiée par la fonction, un entier non. Pour « changer » un entier, la fonction renvoie la nouvelle valeur, et l'appelant la réaffecte.

L'erreur classique

⚠️ Croire que b = a copie la liste. Le cas typique : tu veux garder l'état initial d'une liste avant de la modifier, tu écris sauve = t, puis tu modifies t… et sauve a changé avec elle. Il fallait sauve = list(t).

⚠️ Une fonction qui modifie sa liste sans le dire. Une fonction qui écrit dans son paramètre modifie la liste de l'appelant, même quand ce n'était pas le but :

def dernier_double(t):
    t[-1] = 2 * t[-1]
    return t[-1]

notes = [8, 12, 15]
print(dernier_double(notes))
print(notes)
30
[8, 12, 30]

La fonction devait seulement calculer le double de la dernière note ; elle a réécrit la liste notes. Si la liste ne doit pas changer, calcule dans une expression (return 2 * t[-1]) ou travaille sur une copie.

Réponse. b = a ne copie pas : a et b désignent la même liste, donc a affiche [99, 2, 3], et a is b vaut True. Copie : b = list(a), une nouvelle liste (indépendante tant qu'elle contient des nombres). Après f(u) et g(k), u vaut [5, 0] (liste modifiée sur place) et k vaut 5 (un entier ne se modifie pas). t.append(x) modifie la liste de l'appelant ; t = t + [x] fabrique une nouvelle liste locale, perdue si on ne la renvoie pas.
Faire cet exercice dans l'app →

S'entraîner davantage sur algorithmique & programmation (python)

18 exercices d'entraînement supplémentaires sur ce chapitre, plus le palier approfondissement, les quiz, le tuteur IA et les PDF à imprimer — dans l'app Maths Post-Bac.