Solutions — série 02-5 (erreurs de logique)
Rappel de la règle d'or : on n'ouvre ce fichier qu'après avoir terminé (ou sérieusement séché sur) la série. Pour cette série, compare aussi ta MÉTHODE : as-tu localisé chaque bug avec des espions, ou au hasard ?
Exercice facile a — la voyelle universelle
Raisonnement — Suspect n°1 de la leçon : des opérandes nus dans un or. Python lit (letter == "a") or ("e") or ("i") ... — dès que la première comparaison est fausse, le or évalue "e" : chaîne non vide, truthy. Prédiction pour z : Voyelle. Le else est du code mort.
Solution
# ex-02-5-a.py — deux corrections valables
letter = input("Une lettre ? ")
# correction 1 : chaque option a sa comparaison complète (leçon 02-2)
if letter == "a" or letter == "e" or letter == "i" or letter == "o" or letter == "u":
print("Voyelle")
else:
print("Consonne")
Ou, plus court, avec l'appartenance de la leçon 02-4 :
if letter in "aeiou":
print("Voyelle")
else:
print("Consonne")
Vérification : z → Consonne, a → Voyelle.
Pourquoi ça marche — Version 1 : cinq comparaisons complètes, chacune vaut vraiment True/False. Version 2 : in teste la présence du caractère dans la chaîne des voyelles — une seule condition, aucune place pour l'opérande nu.
Erreur classique sur cet exercice — Diagnostiquer le bug mais corriger seulement les deux premiers or — relire TOUTE la chaîne. Et noter la limite de la version 2 : si l'utilisateur tape ae (deux lettres), "ae" in "aeiou" est True (sous-chaîne !) — la version 1 dirait Consonne. Aucune n'est « fausse » : elles répondent à des questions légèrement différentes. Savoir laquelle tu poses, c'est déjà du débogage.
Variante plus difficile — Durcis la version 2 : refuse les saisies qui ne font pas exactement un caractère… sans len() (⏩ 01-3/niveau 3). Piste tordue mais faisable avec ce que tu sais : une boucle for qui compte les caractères de la saisie.
Exercice facile b — l'indentation qui déplace le print
Raisonnement — Version 1 : le print est hors du bloc → il s'exécute UNE fois, après la boucle, avec la valeur finale. Version 2 : dans le bloc → il s'exécute à CHAQUE tour, montrant la progression.
Solution — sorties vérifiées :
version 1 : version 2 :
6 2
4
6
Pourquoi ça marche — Trois tours, count += 2 par tour → 2, 4, 6. La version 1 n'imprime que l'état final (6) ; la version 2 photographie chaque étape. AUCUNE des deux n'est « la bonne » : tout dépend de l'intention — et c'est exactement pour ça que ce bug est silencieux.
Erreur classique sur cet exercice — Prédire 0 pour la version 1 (« le print est hors de la boucle donc il ne voit pas count ») : hors du BLOC ne veut pas dire hors de PORTÉE — count existe toujours après la boucle, avec sa dernière valeur. (La portée des variables, la vraie, ⏩ niveau 4.)
Variante plus difficile — Ajoute une version 3 : le print(count) indenté DEUX fois (à l'intérieur d'un if count > 2: dans la boucle). Prédis, vérifie. Trois indentations, trois programmes différents — avec les mêmes lignes.
Exercice facile c — le compte à rebours amputé
Raisonnement — range(5, 1, -1) : première valeur 5, on descend, on s'arrête AVANT 1 → 5 4 3 2. Le 1 manque : stop exclu, même en descendant.
Solution
# ex-02-5-c.py — version corrigée
for n in range(5, 0, -1):
print(n, end=" ")
Sortie : 5 4 3 2 1. La règle qui l'évite : écrire première et dernière valeur à la main avant de taper le range — dernière voulue = 1, donc stop = 0.
Pourquoi ça marche — En pas négatif, stop = dernière valeur voulue − 1. La symétrie avec la montée (range(1, 6) pour 1…5) se vérifie : stop toujours UN CRAN au-delà de la dernière valeur, dans le sens de la marche.
Erreur classique sur cet exercice — « Corriger » en range(5, -1, -1) : affiche 5 4 3 2 1 0 — sur-correction, l'off-by-one dans l'autre sens. Toujours re-tester APRÈS la correction : la moitié des corrections de bugs de bornes créent le bug jumeau.
Variante plus difficile — Réécris-le en while et compare : où l'off-by-one peut-il se nicher dans chaque version ? (range : le stop ; while : l'opérateur > vs >=. Même bug, deux déguisements.)
Exercice moyen a — la caisse à 20 EUR
Raisonnement — Attendu : Total : 60 (3 articles × 20). Affiché : autre chose. Espions dans la boucle :
total = 0
for item in range(1, 3):
price = 20
print(f"DEBUG item={item} price={price} total={total}")
total += price
print(f"Total : {total}")
Le DEBUG ne sort que DEUX fois (item=1, item=2) : bug 1, off-by-one — range(1, 3) fait 2 tours, pas 3. Et total reste à 0 pendant la boucle : bug 2, indentation — le total += price est HORS du bloc, exécuté une seule fois après. Sortie du programme buggé : Total : 20.
Solution
# ex-02-5-d.py — version corrigée
total = 0
for item in range(1, 4): # bug 1 : 3 tours = range(1, 4)
price = 20
total += price # bug 2 : accumulation DANS la boucle
# print(f"DEBUG item={item} total={total}") # espions, commentés
print(f"Total : {total}")
Sortie : Total : 60.
Pourquoi ça marche — 3 tours × 20 accumulés à CHAQUE tour. Les espions ont montré les deux écarts d'un coup : nombre de tours (compte des lignes DEBUG) et accumulation gelée (la valeur de total dans les lignes DEBUG). C'est la force de la méthode : un espion bien placé attrape plusieurs bugs.
Erreur classique sur cet exercice — Corriger l'indentation, voir Total : 40, et « corriger » en price = 30 pour obtenir 60 : maquiller le symptôme au lieu de compter les tours. Quand un résultat est faux d'un FACTEUR simple (ici 2 tours au lieu de 3), suspecte le nombre de tours, pas les valeurs.
Variante plus difficile — Le même programme avec des prix différents saisis par l'utilisateur (float(input(...)) dans la boucle) : lequel des deux bugs deviendrait BRUYANT ? (Le bug d'indentation : price n'existerait pas encore à la première exécution de la ligne sortie de la boucle… NameError ? Non plus — elle s'exécute APRÈS la boucle, price existe. Réfléchis bien : les deux resteraient silencieux. D'où la méthode.)
Exercice moyen b — le contrôle d'accès intermittent
Raisonnement — La condition : member == "oui" or age >= 18, avec age en str. Court-circuit (leçon 02-2) : si member == "oui" est VRAI, le côté droit n'est jamais évalué — aucun crash, tout marche. Si le visiteur n'est pas membre, Python évalue age >= 18… une str contre un int :
TypeError: '>' not supported between instances of 'str' and 'int'
(avec >= dans le message réel : '>=' not supported...). Le bug bruyant était masqué par le court-circuit pour TOUS les membres : voilà le crash intermittent.
Solution
# ex-02-5-e.py — version corrigée
member = input("Membre ? (oui/non) ")
age = int(input("Age ? ")) # LA correction : conversion à la saisie
if member == "oui" or age >= 18:
print("Bienvenue")
else:
print("Refuse")
Les quatre combinaisons : oui/15 → Bienvenue ; oui/25 → Bienvenue ; non/25 → Bienvenue ; non/15 → Refuse.
Pourquoi ça marche — La conversion au moment de la saisie (réflexe 01-2) rend la comparaison homogène, que le court-circuit l'évalue ou non. La leçon durable : « mon programme a marché » ≠ « mon programme est juste » — il a peut-être juste emprunté un chemin qui contourne le bug. D'où le test des QUATRE combinaisons, systématique.
Erreur classique sur cet exercice — Corriger en convertissant dans la condition (int(age) >= 18) : ça marche, mais la conversion se répète à chaque usage futur de age, et un age resté str traînera ailleurs. Convertis à la FRONTIÈRE (la saisie), une fois pour toutes. Autre erreur : tester seulement non/15 après correction — re-tester TOUTES les combinaisons, pas juste celle qui plantait.
Variante plus difficile — Remets le bug, et trouve une TROISIÈME saisie piégeuse : que fait member = "OUI" (majuscules) ? (Silencieusement refusé-ou-crashé : "OUI" == "oui" est False — la casse, encore. ⏩ La parade .lower() arrive avec les méthodes de chaînes.)
Exercice moyen c — le quiz qui plafonne à 1
Raisonnement — Déroulé papier avec trois bonnes réponses : tour 1 — answer == 2 + q vrai → score = 1. Tour 2 — vrai → score = 1. Tour 3 — score = 1. Affiché : Score : 1/3. Le compteur n'accumule pas : score = 1 assigne au lieu d'incrémenter. Ce n'est PAS un bug d'indentation (le if et son bloc sont au bon niveau ; le print final aussi) : c'est un bug d'opérateur — = au lieu de +=. Justification demandée par l'énoncé : les lignes sont au bon endroit, c'est leur CONTENU qui est faux.
Solution
# ex-02-5-f.py — version corrigée
score = 0
for q in range(1, 4):
answer = int(input(f"Question {q} : 2 + {q} ? "))
if answer == 2 + q:
score += 1 # accumule au lieu d'écraser
print(f"Score : {score}/3")
Vérifié : réponses 3, 4, 5 → Score : 3/3 ; réponses 3, 9, 5 → Score : 2/3.
Pourquoi ça marche — score += 1 relit l'ancienne valeur et ajoute : le motif compteur (02-3). score = 1 colle l'étiquette sur 1, quel que soit le passé — d'où le plafond. (Et le score 0 de l'énoncé ? Trois mauvaises réponses : le if n'est jamais vrai. Les deux symptômes — 0 ou 1, jamais plus — désignaient ensemble l'assignation écrasante.)
Erreur classique sur cet exercice — Conclure « bug d'indentation » parce que la leçon parle d'indentation : le débogage commence par OBSERVER (dérouler, espionner), pas par pattern-matcher sur la leçon du jour. Un print(f"DEBUG score={score}") dans la boucle montrait 1 1 1 — signature d'un écrasement, pas d'un saut de bloc.
Variante plus difficile — Introduis EXPRÈS le bug d'indentation cousin (le print final indenté dans la boucle) et prédis la sortie exacte avec trois bonnes réponses AVANT d'exécuter. (Trois lignes de score intermédiaires : 1/3, 2/3, 3/3 — utile déguisé en bug, ou bug déguisé en utile ?)
Exercice difficile a — FizzBuzz
Raisonnement — Quatre cas par nombre : multiple de 15 (les deux), multiple de 3 seul, multiple de 5 seul, ni l'un ni l'autre. L'ordre est LA décision : « multiple de 15 » implique « multiple de 3 » — testé après, il serait masqué (erreur n°6 de 02-1, l'elif dans le désordre). Donc : le plus exigeant d'abord.
Solution
# ex-02-5-g.py
for n in range(1, 31):
if n % 15 == 0: # ou : n % 3 == 0 and n % 5 == 0
print("FizzBuzz", end=" ")
elif n % 3 == 0:
print("Fizz", end=" ")
elif n % 5 == 0:
print("Buzz", end=" ")
else:
print(n, end=" ")
print()
Sortie vérifiée :
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz
Le 15 et le 30 affichent bien FizzBuzz.
Pourquoi ça marche — n % 15 == 0 capte les multiples communs AVANT que n % 3 == 0 ne puisse les intercepter. range(1, 31) : le réflexe « stop = dernier + 1 » (30 inclus — vérifie que ta ligne se termine par FizzBuzz). Un seul print par nombre : la chaîne elif garantit l'exclusivité des cas.
Erreur classique sur cet exercice — LE classique mondial : tester n % 3 d'abord → le 15 affiche Fizz. C'est l'exercice d'entretien d'embauche le plus célèbre du monde précisément parce qu'il teste l'ordre des conditions — pas la syntaxe. Autre version : quatre if indépendants sans elif → le 15 affiche FizzBuzz ET Fizz ET Buzz.
Variante plus difficile — Sans le cas 15 explicite : construis le message par CONCATÉNATION (message = "" ; multiple de 3 → message += "Fizz" ; de 5 → += "Buzz" ; message resté vide → le nombre — la truthiness de 02-2 sert enfin). Élégant, extensible (ajoute 7 → "Bazz"), et un cran plus abstrait : la version chaîne elif reste parfaitement défendable.
Mini-projet — « menu de caisse »
Raisonnement — Squelette français d'abord : « tant que le choix n'est pas quitter : afficher le menu, lire le choix ; si 1 → lire un prix, l'ajouter, compter ; si 2 → afficher le total ; si 3 → dire au revoir ; sinon → signaler ». État : total (accumulateur float) et item_count (compteur), initialisés AVANT la boucle. Condition de sortie : sur la valeur du choix — amorcée à "" pour garantir l'entrée en boucle.
Solution
# cash_menu.py
total = 0.0
item_count = 0
choice = "" # amorçage : différent de "3"
while choice != "3":
print("1. Ajouter un article")
print("2. Afficher le total")
print("3. Quitter")
choice = input("Ton choix : ")
if choice == "1":
price = float(input("Prix de l'article ? "))
total += price
item_count += 1
print(f"Ajoute ! ({item_count} article(s), total {total:.2f} EUR)")
elif choice == "2":
print(f"Total : {total:.2f} EUR ({item_count} article(s))")
elif choice == "3":
print("Au revoir !")
else:
print(f"Choix invalide : {choice}")
Session vérifiée (saisies 1, 12.50, 9, 2, 1, 7.5, 2, 3) — extraits :
Ton choix : 1
Prix de l'article ? 12.50
Ajoute ! (1 article(s), total 12.50 EUR)
Ton choix : 9
Choix invalide : 9
Ton choix : 2
Total : 12.50 EUR (1 article(s))
...
Ton choix : 3
Au revoir !
Pourquoi ça marche — La synthèse des cinq leçons, pièce par pièce : while choice != "3" (02-3, condition d'arrêt + amorçage) ; choice == "1" en CHAÎNE, pas en entier (piège 02-1) ; le else attrape l'invalide et la boucle re-affiche le menu — pas de plantage, pas de sortie (02-1) ; total/item_count initialisés hors boucle (02-3, et le bug 9-a de cette série) ; :.2f pour l'argent (01-2). Le choix 3 traverse le elif (le message d'adieu) PUIS la condition du while le voit : sortie propre sans break.
Erreur classique sur cet exercice — Comparer choice == 1 (l'entier) dans les branches : TOUT choix tombe dans Choix invalide, aucun achat n'aboutit — mais la sortie, elle, marche encore (le while compare bien des chaînes). Symptôme déroutant : un menu qui « tourne mais refuse tout » — l'espion print(choice, type(choice)) le règle en dix secondes. Autre classique : total = 0 DANS la boucle : le total se remet à zéro à chaque passage au menu — chaque affichage ne montre que le dernier article.
Variante plus difficile — (1) Valide le prix : redemande tant qu'il est négatif ou nul (un while dans le while). (2) Choix 4 : annuler le dernier article (il faut mémoriser le dernier prix ajouté). (3) Quand tu auras les listes (⏩ niveau 3) : garde TOUS les prix et affiche un ticket détaillé — reviens alors améliorer cette caisse, elle est faite pour durer.