# Chapitre 08 — Erreurs, exceptions et débogage > **Durée estimée** : 4 à 5 heures (cours + exercices + mini-projet) > > **Prérequis** : [Chapitre 07 — Fichiers, CSV et JSON](07-fichiers-csv-json.md) (et tout ce qui précède : variables, conditions, boucles, structures de données, fonctions). > > **Objectifs** — À la fin de ce chapitre, tu sauras : > - lire un *traceback* de bas en haut et trouver en quelques secondes la ligne fautive ; > - reconnaître les exceptions les plus courantes (`ValueError`, `TypeError`, `KeyError`, `IndexError`, `FileNotFoundError`, `ZeroDivisionError`…) et comprendre ce qu'elles signifient ; > - intercepter les erreurs avec `try` / `except` / `else` / `finally` sans masquer les vrais bugs ; > - lever tes propres erreurs avec `raise` et créer tes propres types d'exceptions ; > - déboguer un programme avec `print`, `breakpoint()` et le débogueur de VS Code (points d'arrêt, pas à pas) ; > - utiliser `assert` pour vérifier tes hypothèses pendant le développement. --- ## Cours ### 1. Les erreurs ne sont pas tes ennemies Quand tu débutes, voir un gros pavé rouge s'afficher dans le terminal fait peur. On a l'impression d'avoir « cassé » quelque chose. En réalité, un message d'erreur est **un cadeau** : Python t'explique précisément *ce qui* ne va pas, *où*, et *dans quel contexte*. Les développeurs expérimentés ne font pas moins d'erreurs que toi ; ils savent juste **les lire vite**. Il existe deux grandes familles d'erreurs : 1. **Les erreurs de syntaxe** (`SyntaxError`, `IndentationError`) : ton code n'est pas du Python valide. Python refuse de le lancer **avant même** d'exécuter la première ligne. C'est comme une lettre écrite dans une langue que le facteur ne comprend pas : il ne la distribue pas. 2. **Les exceptions** : ton code est grammaticalement correct, mais quelque chose se passe mal **pendant l'exécution** (diviser par zéro, ouvrir un fichier qui n'existe pas, convertir `"douze"` en entier…). La lettre est bien écrite, mais l'adresse n'existe pas. ```python # Erreur de syntaxe : il manque les deux-points if 3 > 2 print("oui") ``` ``` File "/home/toi/cours/mon_script.py", line 2 if 3 > 2 ^ SyntaxError: expected ':' ``` Ici, **rien** ne s'exécute : Python a lu tout le fichier, vu l'erreur, et s'est arrêté. Les messages de syntaxe de Python 3.12+ sont très parlants : `expected ':'` veut dire « j'attendais deux-points ». Ce chapitre parle surtout de la deuxième famille : les **exceptions**, qu'on peut anticiper, intercepter et même créer. ### 2. Lire un traceback (de bas en haut) Prenons un vrai programme, enregistré dans `mon_script.py` : ```python def calculer_moyenne(notes): total = sum(notes) return total / len(notes) def afficher_bilan(eleve, notes): moyenne = calculer_moyenne(notes) print(f"{eleve} : {moyenne:.2f}") afficher_bilan("Alice", [12, 15, 9]) afficher_bilan("Bob", []) ``` Exécution : ``` Alice : 12.00 Traceback (most recent call last): File "/home/toi/cours/mon_script.py", line 12, in afficher_bilan("Bob", []) ~~~~~~~~~~~~~~^^^^^^^^^^^ File "/home/toi/cours/mon_script.py", line 7, in afficher_bilan moyenne = calculer_moyenne(notes) File "/home/toi/cours/mon_script.py", line 3, in calculer_moyenne return total / len(notes) ~~~~~~^~~~~~~~~~~~ ZeroDivisionError: division by zero ``` Le mot *traceback* signifie « retracer le chemin ». Python te montre **la pile d'appels** : la suite de fonctions qui s'appelaient les unes les autres au moment du drame. L'en-tête le dit clairement : `most recent call last`, « l'appel le plus récent en dernier ». Donc **on lit de bas en haut** : 1. **La dernière ligne** est la plus importante : `ZeroDivisionError: division by zero`. Elle donne le **type** d'erreur (avant les deux-points) et un **message** (après). Ici : une division par zéro. 2. **Juste au-dessus**, le dernier bloc `File ...` indique **où** l'erreur s'est produite : fichier `mon_script.py`, **ligne 3**, dans la fonction `calculer_moyenne`, sur l'instruction `return total / len(notes)`. Les petits `~~~~^~~~` soulignent l'opération exacte qui a échoué : la division `/`. 3. **En remontant encore**, tu vois **qui a appelé** cette fonction : `afficher_bilan`, ligne 7. 4. **Tout en haut**, le point de départ : la ligne 12 du programme principal (`` désigne le code qui n'est dans aucune fonction), avec l'appel `afficher_bilan("Bob", [])`. On reconstitue l'histoire : *la ligne 12 appelle `afficher_bilan` avec une liste vide → qui appelle `calculer_moyenne` → qui divise par `len([])`, c'est-à-dire 0 → boum.* > 💡 **Analogie** : un traceback, c'est comme une enquête policière racontée à l'envers. La dernière ligne, c'est le crime. Les blocs au-dessus, ce sont les témoins, du plus proche au plus lointain. Le coupable n'est pas toujours là où le crime a eu lieu : ici, la division est « innocente », c'est la ligne 12 qui a envoyé une liste vide ! **Méthode en 3 questions** devant n'importe quel traceback : | Question | Où regarder | |---|---| | *Quoi ?* Quel type d'erreur ? | Dernière ligne, avant `:` | | *Où ?* Quelle ligne de **mon** code ? | Le bloc `File` le plus bas qui pointe vers **ton** fichier | | *Pourquoi ?* D'où viennent les mauvaises valeurs ? | Remonter les blocs `File` vers le haut | > ⚠️ Quand tu utilises une bibliothèque, certains blocs `File` pointent vers des fichiers que tu n'as pas écrits (par exemple `.../lib/python3.12/json/decoder.py`). Ignore-les dans un premier temps : cherche **le dernier bloc qui mentionne ton fichier**. Le bug est presque toujours là. ### 3. Le tour des exceptions courantes Voici les exceptions que tu croiseras 95 % du temps. Chaque exemple a été exécuté pour de vrai ; seule la dernière ligne du traceback est montrée. #### `ValueError` — bon type, mauvaise valeur ```python int("douze") # ValueError: invalid literal for int() with base 10: 'douze' float("3,5") # la virgule française ne passe pas ! # ValueError: could not convert string to float: '3,5' ``` `int()` attend bien une chaîne, mais pas *celle-là*. Très fréquent avec `input()`. #### `TypeError` — mauvais type ```python "Âge : " + 30 # TypeError: can only concatenate str (not "int") to str len(5) # TypeError: object of type 'int' has no len() ``` Tu essaies de faire une opération qui n'a pas de sens pour ce type. Solution typique : convertir (`str(30)`) ou utiliser une f-string. #### `KeyError` — clé absente d'un dictionnaire ```python prix = {"pomme": 1.2} prix["poire"] # KeyError: 'poire' ``` Le message affiche **la clé fautive**. Pense à `prix.get("poire")` ou au test `if "poire" in prix`. #### `IndexError` — indice hors de la liste ```python valeurs = [10, 20, 30] valeurs[3] # IndexError: list index out of range ``` Une liste de 3 éléments a les indices 0, 1 et 2. L'erreur classique « décalée de un ». #### `FileNotFoundError` — fichier introuvable ```python open("notes.txt") # FileNotFoundError: [Errno 2] No such file or directory: 'notes.txt' ``` Soit le nom est faux, soit tu n'exécutes pas le script depuis le bon dossier (revois le [chapitre 07](07-fichiers-csv-json.md) sur les chemins). #### `ZeroDivisionError` — division par zéro ```python 10 / 0 # ZeroDivisionError: division by zero ``` Vaut aussi pour `//` et `%`. #### Deux autres très fréquentes ```python print(totl) # faute de frappe dans le nom # NameError: name 'totl' is not defined "abc".push("d") # cette méthode n'existe pas pour les str # AttributeError: 'str' object has no attribute 'push' ``` #### La hiérarchie des exceptions Les exceptions sont rangées en **famille**, comme un arbre généalogique. Voici la partie qui t'intéresse : ``` BaseException ├── KeyboardInterrupt (Ctrl+C) ├── SystemExit (sys.exit()) └── Exception ← mère de toutes les erreurs « normales » ├── ArithmeticError │ └── ZeroDivisionError ├── LookupError │ ├── IndexError │ └── KeyError ├── OSError │ └── FileNotFoundError ├── ValueError ├── TypeError ├── NameError └── AttributeError ``` Pourquoi c'est utile ? Parce qu'**intercepter un parent intercepte aussi tous ses enfants**. `except LookupError` attrape à la fois les `KeyError` et les `IndexError`. Et `except Exception` attrape presque tout… ce qui est souvent **trop** (on y revient dans les pièges). Tu peux vérifier la parenté avec `issubclass` : ```python print(issubclass(KeyError, LookupError)) # True print(issubclass(FileNotFoundError, OSError)) # True print(issubclass(KeyboardInterrupt, Exception)) # False ``` ### 4. Intercepter une exception : `try` / `except` Jusqu'ici, une exception arrête net le programme. Avec `try` / `except`, tu dis à Python : « essaie ceci, et **si** telle erreur survient, fais plutôt cela ». ```python texte = input("Ton âge : ") try: age = int(texte) print(f"Dans 10 ans, tu auras {age + 10} ans.") except ValueError: print(f"« {texte} » n'est pas un nombre entier.") print("Le programme continue.") ``` Avec l'entrée `vingt` : ``` Ton âge : vingt « vingt » n'est pas un nombre entier. Le programme continue. ``` Avec l'entrée `20` : ``` Ton âge : 20 Dans 10 ans, tu auras 30 ans. Le programme continue. ``` Le déroulement : - Python exécute le bloc `try` ligne par ligne. - Si **aucune** erreur ne survient, le bloc `except` est **ignoré**. - Si une `ValueError` survient, Python **abandonne le reste du bloc `try`** (le `print` n'est pas exécuté) et saute dans `except ValueError`. - Si une **autre** erreur survient (par exemple une `TypeError`), elle n'est **pas** interceptée : le programme plante normalement, avec son traceback. > 💡 **Analogie** : `try` / `except`, c'est le filet sous le trapéziste. Le trapéziste fait son numéro (`try`). S'il tombe d'une certaine façon prévue (`except ValueError`), le filet le rattrape. Mais un filet ne sert que pour les chutes prévues : si le chapiteau prend feu, le filet ne sert à rien. #### Redemander jusqu'à obtenir une bonne valeur Combiné avec une boucle `while`, c'est le schéma le plus utilisé du chapitre : ```python while True: try: quantite = int(input("Quantité : ")) break # on ne sort que si la conversion a réussi except ValueError: print("Merci d'entrer un nombre entier.") print(f"Tu as commandé {quantite} article(s).") ``` Avec les entrées `abc`, `2.5` puis `3` : ``` Quantité : abc Merci d'entrer un nombre entier. Quantité : 2.5 Merci d'entrer un nombre entier. Quantité : 3 Tu as commandé 3 article(s). ``` #### Plusieurs `except` Un même `try` peut avoir plusieurs blocs `except`, testés **dans l'ordre** : ```python def lire_element(valeurs, saisie): try: indice = int(saisie) return valeurs[indice] except ValueError: return "Il faut un nombre." except IndexError: return f"Indice entre 0 et {len(valeurs) - 1}, s'il te plaît." fruits = ["pomme", "kiwi", "mangue"] print(lire_element(fruits, "1")) # kiwi print(lire_element(fruits, "deux")) # Il faut un nombre. print(lire_element(fruits, "7")) # Indice entre 0 et 2, s'il te plaît. ``` Si deux erreurs doivent être traitées **de la même façon**, regroupe-les dans un **tuple** (les parenthèses sont obligatoires) : ```python def diviser_textes(a, b): try: return int(a) / int(b) except (ValueError, ZeroDivisionError): return None print(diviser_textes("10", "4")) # 2.5 print(diviser_textes("10", "0")) # None print(diviser_textes("dix", "2")) # None ``` > ⚠️ L'ordre compte : si tu mets `except LookupError` **avant** `except KeyError`, le second ne sera jamais atteint, car le parent attrape déjà l'enfant. Mets toujours **les exceptions les plus précises en premier**. ### 5. Récupérer l'objet erreur avec `as` Tu peux donner un nom à l'exception attrapée pour afficher son message : ```python try: int("abc") except ValueError as erreur: print("Type :", type(erreur).__name__) print("Message :", erreur) ``` ``` Type : ValueError Message : invalid literal for int() with base 10: 'abc' ``` C'est pratique pour informer l'utilisateur **sans** lui montrer un traceback complet : ```python from pathlib import Path chemin = Path("fichier_qui_nexiste_pas.txt") try: contenu = chemin.read_text(encoding="utf-8") except FileNotFoundError as erreur: print(f"Impossible d'ouvrir le fichier : {erreur.filename}") ``` ``` Impossible d'ouvrir le fichier : fichier_qui_nexiste_pas.txt ``` ### 6. Les blocs `else` et `finally` La structure complète ressemble à ceci : ```python try: ... # le code « risqué » except UneErreur: ... # exécuté SI l'erreur survient else: ... # exécuté SI AUCUNE erreur n'est survenue finally: ... # exécuté TOUJOURS, quoi qu'il arrive ``` #### `else` : « tout s'est bien passé » Pourquoi ne pas tout mettre dans le `try` ? Parce qu'on veut que le `try` ne contienne **que** la ligne susceptible d'échouer. Sinon, on risque d'attraper par erreur une exception venant d'une autre ligne. ```python import json from pathlib import Path chemin = Path("config.json") chemin.write_text('{"theme": "sombre", "taille": 14}', encoding="utf-8") try: texte = chemin.read_text(encoding="utf-8") except FileNotFoundError: print("Pas de configuration, on utilise les valeurs par défaut.") config = {} else: config = json.loads(texte) print("Configuration chargée.") print(config) ``` ``` Configuration chargée. {'theme': 'sombre', 'taille': 14} ``` #### `finally` : « quoi qu'il arrive » Le bloc `finally` s'exécute **toujours** : succès, erreur interceptée, erreur non interceptée, et même `return` dans le `try`. On s'en sert pour faire le ménage (fermer une connexion, afficher un message de fin…). ```python def diviser(a, b): try: resultat = a / b except ZeroDivisionError: print("Division par zéro évitée.") resultat = None else: print("Division réussie.") finally: print("--- fin du calcul ---") return resultat print(diviser(10, 2)) print(diviser(1, 0)) ``` ``` Division réussie. --- fin du calcul --- 5.0 Division par zéro évitée. --- fin du calcul --- None ``` > 💡 Pour les fichiers, `with open(...)` (vu au [chapitre 07](07-fichiers-csv-json.md)) fait déjà le ménage à ta place : il ferme le fichier même en cas d'erreur. C'est un `finally` intégré ! ### 7. Lever une exception : `raise` Jusqu'ici, on **subissait** les exceptions. Tu peux aussi les **déclencher** toi-même avec `raise`, pour signaler qu'une situation est anormale. ```python def calculer_moyenne(notes): if not notes: raise ValueError("Impossible de calculer la moyenne d'une liste vide.") return sum(notes) / len(notes) print(calculer_moyenne([12, 15, 9])) print(calculer_moyenne([])) ``` ``` 12.0 Traceback (most recent call last): File "/home/toi/cours/mon_script.py", line 8, in print(calculer_moyenne([])) ~~~~~~~~~~~~~~~~^^^^ File "/home/toi/cours/mon_script.py", line 3, in calculer_moyenne raise ValueError("Impossible de calculer la moyenne d'une liste vide.") ValueError: Impossible de calculer la moyenne d'une liste vide. ``` Pourquoi lever une erreur plutôt que de renvoyer `None` ou d'afficher un message ? Parce qu'une fonction **ne sait pas** comment l'appelant veut réagir. Peut-être qu'il veut redemander une saisie, peut-être écrire dans un journal, peut-être arrêter. En levant une exception, la fonction dit « je ne peux pas faire mon travail, voici pourquoi » et **laisse l'appelant décider** : ```python def calculer_moyenne(notes): if not notes: raise ValueError("Impossible de calculer la moyenne d'une liste vide.") return sum(notes) / len(notes) classe = {"Alice": [12, 15, 9], "Bob": [], "Chloé": [18, 16]} for eleve, notes in classe.items(): try: print(f"{eleve} : {calculer_moyenne(notes):.2f}") except ValueError as erreur: print(f"{eleve} : pas de moyenne ({erreur})") ``` ``` Alice : 12.00 Bob : pas de moyenne (Impossible de calculer la moyenne d'une liste vide.) Chloé : 17.00 ``` #### Relancer une exception Dans un `except`, un `raise` **seul** relance l'exception en cours. Utile pour noter qu'il y a eu un problème sans l'étouffer : ```python def convertir(texte): try: return int(texte) except ValueError: print(f"[journal] conversion ratée pour {texte!r}") raise # on laisse remonter l'erreur d'origine try: convertir("12a") except ValueError: print("L'appelant a bien reçu l'erreur.") ``` ``` [journal] conversion ratée pour '12a' L'appelant a bien reçu l'erreur. ``` Tu peux aussi **transformer** une erreur en une autre plus parlante, en gardant la trace de l'originale avec `from` : ```python try: int("x") except ValueError as erreur: raise RuntimeError("Lecture du fichier de scores impossible") from erreur ``` Le traceback affichera alors les deux erreurs, reliées par la phrase *The above exception was the direct cause of the following exception*. ### 8. Créer tes propres exceptions Les exceptions intégrées sont génériques. Dans un vrai programme, il est souvent plus clair d'avoir des erreurs **qui parlent ton langage métier** : `SoldeInsuffisantError`, `MotDePasseTropCourtError`… Pour cela, on utilise une **classe**. Les classes seront expliquées en détail au [chapitre 10](10-poo-classes-objets.md) ; pour l'instant, retiens simplement cette **syntaxe à apprendre par cœur** : ```python class SoldeInsuffisantError(Exception): pass ``` Lis-la comme : « je crée un nouveau type d'erreur, appelé `SoldeInsuffisantError`, qui est une sorte d'`Exception` ». Le `pass` signifie « rien d'autre à ajouter ». Par convention, le nom se termine par `Error`. Tu l'utilises exactement comme une exception intégrée : ```python class SoldeInsuffisantError(Exception): pass def retirer(solde, montant): if montant > solde: raise SoldeInsuffisantError( f"Solde insuffisant : {solde} € disponibles, {montant} € demandés" ) return solde - montant solde = 50 try: solde = retirer(solde, 80) except SoldeInsuffisantError as erreur: print("Opération refusée.", erreur) print(f"Solde final : {solde} €") ``` ``` Opération refusée. Solde insuffisant : 50 € disponibles, 80 € demandés Solde final : 50 € ``` Tu peux même créer **ta propre petite famille** : une erreur « mère » pour ton programme, et des erreurs « filles » plus précises. L'appelant pourra alors attraper toutes les erreurs de ton programme d'un coup avec la mère : ```python class BanqueError(Exception): pass class SoldeInsuffisantError(BanqueError): pass class MontantInvalideError(BanqueError): pass def retirer(solde, montant): if montant <= 0: raise MontantInvalideError("Le montant doit être positif.") if montant > solde: raise SoldeInsuffisantError("Solde insuffisant.") return solde - montant for montant in [20, -5, 500]: try: print("Nouveau solde :", retirer(100, montant)) except BanqueError as erreur: print(f"{type(erreur).__name__} : {erreur}") ``` ``` Nouveau solde : 80 MontantInvalideError : Le montant doit être positif. SoldeInsuffisantError : Solde insuffisant. ``` ### 9. Deux philosophies : vérifier avant, ou demander pardon après Il y a deux façons de gérer un cas problématique : - **LBYL** (*Look Before You Leap*, « regarde avant de sauter ») : on teste avec un `if` avant d'agir. - **EAFP** (*Easier to Ask Forgiveness than Permission*, « plus facile de demander pardon que la permission ») : on agit, et on intercepte l'erreur si elle survient. ```python stock = {"pomme": 12, "kiwi": 0} # Style LBYL if "banane" in stock: print(stock["banane"]) else: print("Produit inconnu") # Style EAFP try: print(stock["banane"]) except KeyError: print("Produit inconnu") ``` Les deux sont valables. En Python, le style EAFP est très courant, surtout quand la vérification préalable est compliquée ou incomplète. Exemple : vérifier qu'une chaîne représente un entier valide (`"-12"`, `" 7 "`, `"+3"`…) est pénible avec des `if`, alors que `try: int(texte)` règle la question en une ligne. Règle pratique : **utilise `if` pour les cas prévisibles et fréquents** (un dictionnaire où la clé manque souvent → `.get()`), et **`try` pour les cas exceptionnels ou difficiles à prévoir** (saisie utilisateur, fichiers, réseau). ### 10. Déboguer : trouver les bugs qui ne plantent pas Le pire bug n'est pas celui qui plante : c'est celui qui **donne un résultat faux sans rien dire**. Pas de traceback pour t'aider ! Il faut alors enquêter. Voici trois outils, du plus simple au plus puissant. Prenons un programme bogué : ```python def appliquer_remise(prix, pourcentage): remise = prix * pourcentage // 100 return prix - remise panier = [19.99, 5.50, 12.00] total = 0 for prix in panier: total = appliquer_remise(prix, 10) print(f"Total après remise : {total:.2f} €") ``` ``` Total après remise : 11.00 € ``` Le total devrait être environ 34,24 €. Quelque chose cloche… et même deux choses. #### Outil n° 1 : le *print-debugging* La méthode la plus ancienne et toujours efficace : ajouter des `print` pour **voir** les valeurs à chaque étape. ```python def appliquer_remise(prix, pourcentage): remise = prix * pourcentage // 100 print(f"[DEBUG] prix={prix} remise={remise}") return prix - remise panier = [19.99, 5.50, 12.00] total = 0 for prix in panier: total = appliquer_remise(prix, 10) print(f"[DEBUG] total={total}") print(f"Total après remise : {total:.2f} €") ``` ``` [DEBUG] prix=19.99 remise=1.0 [DEBUG] total=18.99 [DEBUG] prix=5.5 remise=0.0 [DEBUG] total=5.5 [DEBUG] prix=12.0 remise=1.0 [DEBUG] total=11.0 Total après remise : 11.00 € ``` On voit immédiatement deux bugs : 1. `remise=1.0` pour 19,99 € au lieu de 1,999 : c'est `//` (division entière) au lieu de `/`. 2. `total` ne s'accumule pas : il est **remplacé** à chaque tour (`total =` au lieu de `total +=`). Astuce : la f-string avec `=` affiche le nom **et** la valeur, idéal pour déboguer : ```python prix = 19.99 pourcentage = 10 print(f"{prix=} {pourcentage=} {prix * pourcentage / 100=}") ``` ``` prix=19.99 pourcentage=10 prix * pourcentage / 100=1.9989999999999997 ``` (Le `1.9989999999999997` au lieu de `1.999` est normal : c'est l'imprécision des nombres décimaux en machine, vue au [chapitre 01](01-variables-types-operateurs.md).) > ⚠️ Pense à **retirer tes `print` de débogage** une fois le bug corrigé. Les préfixer par `[DEBUG]` permet de les retrouver facilement avec la recherche de VS Code (Ctrl+F). #### Outil n° 2 : `breakpoint()` et le débogueur `pdb` Ajouter des `print` partout devient vite lourd. Python possède un **débogueur intégré** : `pdb`. Il suffit d'écrire `breakpoint()` à l'endroit où tu veux **mettre le programme en pause** : ```python def appliquer_remise(prix, pourcentage): breakpoint() remise = prix * pourcentage / 100 return prix - remise total = appliquer_remise(80, 25) print(total) ``` Quand tu lances le script, il s'arrête et te donne une invite `(Pdb)` où tu peux taper des commandes : ``` > /home/toi/cours/mon_script.py(2)appliquer_remise() -> breakpoint() (Pdb) p prix 80 (Pdb) p pourcentage 25 (Pdb) n > /home/toi/cours/mon_script.py(3)appliquer_remise() -> remise = prix * pourcentage / 100 (Pdb) p remise *** NameError: name 'remise' is not defined (Pdb) n > /home/toi/cours/mon_script.py(4)appliquer_remise() -> return prix - remise (Pdb) p remise 20.0 (Pdb) c 60.0 ``` La ligne marquée `->` est **la prochaine ligne qui va s'exécuter** (pas encore exécutée !). C'est pour ça que `remise` n'existe pas encore au premier essai. Les commandes essentielles : | Commande | Signification | |---|---| | `p variable` | **p**rint : affiche la valeur d'une expression | | `n` | **n**ext : exécute la ligne courante et passe à la suivante (sans entrer dans les fonctions appelées) | | `s` | **s**tep : entre **dans** la fonction appelée sur la ligne courante | | `l` | **l**ist : affiche le code autour de la ligne courante | | `c` | **c**ontinue : reprend l'exécution normale jusqu'au prochain `breakpoint()` | | `q` | **q**uit : arrête le programme | > 💡 N'oublie pas d'enlever `breakpoint()` avant de partager ton code, sinon il se mettra en pause chez tout le monde ! #### Outil n° 3 : le débogueur de VS Code C'est le même principe que `pdb`, mais avec une interface graphique. C'est l'outil que tu utiliseras le plus au quotidien. **1. Poser un point d'arrêt.** Dans VS Code, clique dans la marge **à gauche des numéros de ligne** : un **point rouge** apparaît. Le programme s'arrêtera **juste avant** d'exécuter cette ligne. (Raccourci : place le curseur sur la ligne et appuie sur **F9**.) **2. Lancer en mode débogage.** Au lieu du bouton ▶ « Run », clique sur la petite flèche à côté et choisis **« Python Debugger: Debug Python File »**, ou appuie sur **F5** (choisis « Python File » si VS Code te demande une configuration). L'extension *Python* de Microsoft (installée au [chapitre 00](00-installation-environnement.md)) inclut le débogueur. **3. Observer.** Quand le programme s'arrête sur ton point rouge, la ligne est surlignée en jaune et le panneau de gauche affiche : - **VARIABLES** : toutes les variables locales et globales avec leur valeur actuelle. Tu peux déplier les listes et dictionnaires. - **WATCH** (Espion) : ajoute des expressions à surveiller en continu, par exemple `total` ou `prix * 0.9`. - **CALL STACK** (Pile des appels) : la même information qu'un traceback, en direct. Tu peux aussi survoler une variable avec la souris dans le code pour voir sa valeur. **4. Avancer pas à pas.** Une barre d'outils flottante apparaît en haut : | Bouton | Raccourci | Effet | Équivalent `pdb` | |---|---|---|---| | ▶ Continuer | F5 | Jusqu'au prochain point d'arrêt | `c` | | ⤼ Pas à pas principal (*Step Over*) | F10 | Ligne suivante, sans entrer dans les fonctions | `n` | | ⤓ Pas à pas détaillé (*Step Into*) | F11 | Entre dans la fonction appelée | `s` | | ⤒ Pas à pas sortant (*Step Out*) | Maj+F11 | Termine la fonction courante et remonte | `r` | | ⟳ Redémarrer | Ctrl+Maj+F5 | Relance depuis le début | — | | ■ Arrêter | Maj+F5 | Arrête le programme | `q` | **5. Bonus : la console de débogage.** Pendant la pause, l'onglet **DEBUG CONSOLE** permet de taper n'importe quelle expression Python (`len(panier)`, `prix * 2`…) et de voir le résultat, dans le contexte exact du programme en pause. **6. Points d'arrêt conditionnels.** Clic droit sur un point rouge → **Edit Breakpoint** → **Expression**, par exemple `prix > 10`. Le programme ne s'arrêtera que si la condition est vraie. Indispensable dans une boucle de 10 000 tours ! > 💡 **Analogie** : le débogueur, c'est la télécommande d'un film. Tu peux mettre sur pause (point d'arrêt), avancer image par image (F10), zoomer sur un détail (F11, Variables), ou reprendre la lecture (F5). Le `print`, c'est comme prendre des photos du film en espérant avoir capturé le bon moment. ### 11. `assert` : vérifier tes hypothèses `assert` vérifie qu'une condition est vraie. Si elle est fausse, il lève une `AssertionError` avec le message fourni : ```python def calculer_remise(prix, pourcentage): assert 0 <= pourcentage <= 100, f"pourcentage invalide : {pourcentage}" return prix * pourcentage / 100 print(calculer_remise(50, 20)) print(calculer_remise(50, 150)) ``` ``` 10.0 Traceback (most recent call last): File "/home/toi/cours/mon_script.py", line 7, in print(calculer_remise(50, 150)) ~~~~~~~~~~~~~~~^^^^^^^^^ File "/home/toi/cours/mon_script.py", line 2, in calculer_remise assert 0 <= pourcentage <= 100, f"pourcentage invalide : {pourcentage}" ^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: pourcentage invalide : 150 ``` `assert` sert à **vérifier tes propres hypothèses de programmeur** (« à cet endroit, cette liste ne peut pas être vide »), et à écrire de petits tests rapides : ```python def inverser(texte): return texte[::-1] assert inverser("abc") == "cba" assert inverser("") == "" assert inverser("été") == "été" print("Tous les tests passent.") ``` ``` Tous les tests passent. ``` C'est d'ailleurs exactement ce qu'utilise l'outil de test `pytest` que tu découvriras au [chapitre 13](13-tests-pytest-bonnes-pratiques.md). > ⚠️ **Ne valide jamais une saisie utilisateur avec `assert`**. Python peut être lancé avec l'option `-O` (optimisation) qui **désactive tous les `assert`**. Pour une donnée venant de l'extérieur (saisie, fichier), utilise `if` + `raise ValueError(...)`. | Situation | Outil | |---|---| | La donnée vient de l'utilisateur, d'un fichier, du réseau | `if ...: raise ValueError(...)` | | Tu vérifies que **ton propre code** est cohérent | `assert` | | Tu écris un test | `assert` (puis `pytest`) | ### 12. Une méthode de débogage en 5 étapes Quand un bug te résiste, ne modifie pas le code au hasard. Procède comme un détective : 1. **Reproduire** : trouve les entrées exactes qui provoquent le bug. Un bug qu'on ne sait pas reproduire ne se corrige pas. 2. **Lire** : s'il y a un traceback, applique la méthode des 3 questions (quoi, où, pourquoi). 3. **Isoler** : réduis le problème. Commente des morceaux, teste la fonction suspecte seule avec des valeurs simples. 4. **Observer** : `print`, `breakpoint()` ou VS Code pour comparer **ce que tu crois** que contiennent tes variables avec **ce qu'elles contiennent vraiment**. 5. **Corriger puis vérifier** : corrige **une seule chose à la fois**, relance, et ajoute un `assert` pour que le bug ne revienne pas. > 💡 **La méthode du canard en plastique** : explique ton code ligne par ligne, à voix haute, à un canard en plastique (ou à un ami patient). Très souvent, tu trouves le bug toi-même en cours d'explication, parce que tu es obligé de dire ce que fait *vraiment* chaque ligne. --- ## Pièges fréquents ### Piège 1 — Le `except` nu qui avale tout ```python # ❌ À NE PAS FAIRE def lire_age(texte): try: return int(texte) except: return 0 print(lire_age("25")) print(lire_age(None)) # bug caché : on reçoit 0 au lieu d'une erreur claire ``` Un `except:` sans type attrape **absolument tout** : les fautes de frappe (`NameError`), les mauvais types (`TypeError`), et même **Ctrl+C** (`KeyboardInterrupt`) ! Ton programme ne plante plus… mais il produit des résultats faux en silence, et tu ne peux même plus l'interrompre proprement. C'est le piège n° 1 des débutants. ```python # ✅ Attrape uniquement ce que tu sais gérer def lire_age(texte): try: return int(texte) except ValueError: return 0 ``` Même chose pour `except Exception:` : à réserver au tout dernier filet de sécurité d'un programme, en **affichant** l'erreur, jamais pour l'étouffer. ### Piège 2 — Oublier les parenthèses pour plusieurs exceptions ```python try: int("x") except ValueError, TypeError: pass ``` ``` except ValueError, TypeError: ^^^^^^^^^^^^^^^^^^^^^ SyntaxError: multiple exception types must be parenthesized ``` ✅ Correction : `except (ValueError, TypeError):`. ### Piège 3 — Un `try` trop large ```python # ❌ Tout le bloc est dans le try notes = {"Alice": 14, "Bob": 12} try: nom = "Chloé" note = notes[nom] moyenne = sum(notes.values()) / len(note) # bug : len(note) au lieu de len(notes) print(moyenne) except Exception: print("Élève introuvable") ``` ``` Élève introuvable ``` Le message est **faux** et trompeur : ici c'est bien le `KeyError` qui se déclenche, mais si Chloé existait, le bug `len(note)` (une `TypeError`) serait lui aussi transformé en « Élève introuvable ». Tu chercherais l'erreur au mauvais endroit pendant des heures. ```python # ✅ Le try ne contient que la ligne risquée, et l'exception précise notes = {"Alice": 14, "Bob": 12} nom = "Chloé" try: note = notes[nom] except KeyError: print(f"Élève introuvable : {nom}") else: print(f"{nom} a {note}/20") ``` ### Piège 4 — Mettre le parent avant l'enfant ```python donnees = {"a": 1} try: donnees["b"] except LookupError: print("Problème de recherche") except KeyError: print("Clé absente") # ❌ jamais atteint ``` ``` Problème de recherche ``` `KeyError` est une sorte de `LookupError`, donc le premier `except` gagne toujours. ✅ Mets les exceptions **les plus précises en premier**. ### Piège 5 — Lever une simple chaîne avec `raise` ```python # ❌ raise avec une simple chaîne raise "Erreur grave" ``` ``` TypeError: exceptions must derive from BaseException ``` On ne peut lever que des **objets exception**. ✅ `raise ValueError("Erreur grave")`. ### Piège 6 — Valider des données avec `assert` ```python # ❌ disparaît si Python est lancé avec -O def definir_age(age): assert age >= 0, "âge négatif" return age ``` ✅ Pour une donnée extérieure : ```python def definir_age(age): if age < 0: raise ValueError(f"âge négatif : {age}") return age ``` ### Piège 7 — Oublier de convertir la saisie (et croire à un bug mystérieux) ```python age = input("Âge : ") print(age + 1) ``` ``` TypeError: can only concatenate str (not "int") to str ``` `input()` renvoie **toujours** une chaîne. Le message dit « on ne peut concaténer que des str à une str » : `age` est donc une chaîne. ✅ `age = int(input("Âge : "))`, protégé par un `try` / `except ValueError`. ### Piège 8 — Afficher l'erreur puis continuer comme si de rien n'était ```python try: prix = float("douze") except ValueError: print("Prix invalide") print(prix * 2) ``` ``` Prix invalide Traceback (most recent call last): ... NameError: name 'prix' is not defined ``` Après l'`except`, la variable `prix` n'a jamais été créée. ✅ Soit tu lui donnes une valeur par défaut dans l'`except`, soit tu arrêtes le traitement (`return`, `continue`, boucle de nouvelle saisie…). --- ## Exercices ### Exercice 1 ⭐ — Saisie d'un entier Écris une fonction `demander_entier(message)` qui affiche `message`, lit la saisie de l'utilisateur et la renvoie convertie en `int`. Si la saisie n'est pas un entier, elle affiche `Ce n'est pas un nombre entier, recommence.` et redemande, autant de fois que nécessaire. ``` Ton âge : vingt Ce n'est pas un nombre entier, recommence. Ton âge : 20 Tu as 20 ans. ``` ### Exercice 2 ⭐ — Division sans plantage Écris une fonction `division_sure(a, b)` qui renvoie `a / b`, ou la chaîne `"Division par zéro impossible"` si `b` vaut 0. Utilise `try` / `except`, pas de `if`. ```python print(division_sure(10, 4)) # 2.5 print(division_sure(3, 0)) # Division par zéro impossible ``` ### Exercice 3 ⭐ — Lire un traceback Sans exécuter ce programme, réponds aux trois questions : quel est le type d'erreur ? Quelle ligne de code l'a déclenchée ? Quelle est la cause et comment la corriger ? ```python def prix_ttc(prix_ht): return prix_ht * 1.2 def afficher_facture(articles): for nom, prix in articles.items(): print(f"{nom} : {prix_ttc(prix):.2f} €") panier = {"stylo": 1.5, "cahier": "3.2", "gomme": 0.8} afficher_facture(panier) ``` ``` stylo : 1.80 € Traceback (most recent call last): File "/home/toi/cours/facture.py", line 11, in afficher_facture(panier) ~~~~~~~~~~~~~~~~^^^^^^^^ File "/home/toi/cours/facture.py", line 7, in afficher_facture print(f"{nom} : {prix_ttc(prix):.2f} €") ~~~~~~~~^^^^^^ File "/home/toi/cours/facture.py", line 2, in prix_ttc return prix_ht * 1.2 ~~~~~~~~^~~~~ TypeError: can't multiply sequence by non-int of type 'float' ``` ### Exercice 4 ⭐⭐ — Nettoyer une liste de notes Écris une fonction `convertir_notes(textes)` qui reçoit une liste de chaînes et renvoie un tuple `(notes_valides, rejets)` : - `notes_valides` : la liste des notes converties en `float` ; - `rejets` : la liste des chaînes qui n'ont pas pu être converties **ou** dont la valeur n'est pas entre 0 et 20. Bonus : accepte la virgule française (`"12,5"`). ```python valides, rejets = convertir_notes(["12", "15.5", "abc", "12,5", "25", "", "-1", "8"]) print(valides) # [12.0, 15.5, 12.5, 8.0] print(rejets) # ['abc', '25', '', '-1'] ``` ### Exercice 5 ⭐⭐ — Chargement de configuration Écris une fonction `charger_config(chemin)` qui lit un fichier JSON et renvoie le dictionnaire obtenu. Elle doit : - renvoyer `{"langue": "fr", "volume": 50}` (configuration par défaut) si le fichier n'existe pas, en affichant `Fichier absent, configuration par défaut.` ; - renvoyer la même configuration par défaut si le fichier n'est pas du JSON valide (l'exception s'appelle `json.JSONDecodeError`), en affichant `Fichier corrompu, configuration par défaut.` ; - dans les deux cas, ne jamais planter. ### Exercice 6 ⭐⭐ — Inventaire et exceptions précises On a un stock : `stock = {"pomme": 10, "poire": 4, "kiwi": 0}`. Écris une fonction `retirer_du_stock(stock, produit, quantite)` qui : - lève une `KeyError` (laisse-la se produire naturellement) si le produit n'existe pas ; - lève une `ValueError("quantité invalide : ...")` si la quantité est négative ou nulle ; - lève une `ValueError("stock insuffisant pour ...")` si la quantité demandée dépasse le stock ; - sinon, met à jour le stock. Puis écris une boucle qui teste ces demandes et affiche un message pour chacune : ```python demandes = [("pomme", 3), ("banane", 1), ("poire", 10), ("kiwi", -2), ("poire", 4)] ``` ``` OK : 3 pomme(s) retirée(s) Erreur : produit inconnu 'banane' Erreur : stock insuffisant pour poire (4 disponible(s)) Erreur : quantité invalide : -2 OK : 4 poire(s) retirée(s) Stock final : {'pomme': 7, 'poire': 0, 'kiwi': 0} ``` ### Exercice 7 ⭐⭐⭐ — Ta propre exception Crée une exception `MotDePasseInvalideError`. Écris une fonction `verifier_mot_de_passe(mdp)` qui lève cette exception avec un message précis si : - le mot de passe fait moins de 8 caractères ; - il ne contient aucun chiffre ; - il ne contient aucune majuscule. Sinon, elle renvoie `True`. Écris ensuite un programme qui demande un mot de passe jusqu'à ce qu'il soit valide. ``` Mot de passe : abc Refusé : au moins 8 caractères (actuellement 3) Mot de passe : abcdefgh Refusé : il faut au moins un chiffre Mot de passe : abcdefg1 Refusé : il faut au moins une majuscule Mot de passe : Abcdefg1 Mot de passe accepté ! ``` ### Exercice 8 ⭐⭐⭐ — Chasse aux bugs Ce programme est censé compter les mots d'un texte et afficher les 3 plus fréquents. Il contient **trois bugs** (dont un seul fait planter le programme). Trouve-les avec `print`, `breakpoint()` ou le débogueur de VS Code, et corrige-les. Pour chacun, note la technique qui t'a permis de le trouver. ```python def compter_mots(texte): compteur = {} for mot in texte.split(): mot = mot.lower().strip(".,!?") if mot in compteur: compteur[mot] = 1 else: compteur[mot] = 1 return compteur def top_mots(compteur, n): classement = sorted(compteur.items(), key=lambda paire: paire[1]) return classement[:n] texte = "Le chat dort. Le chien dort aussi. Le chat rêve !" for mot, nombre in top_mots(compter_mots(texte), 3): print(f"{mot} : {nombre}") print("Mot le plus fréquent :", top_mots(compter_mots(texte), 3)[3]) ``` Résultat attendu après correction : ``` le : 3 chat : 2 dort : 2 Mot le plus fréquent : le ``` --- ## Mini-projet — Une calculatrice qui ne plante jamais ### Cahier des charges Tu vas écrire `calculatrice.py`, une calculatrice en ligne de commande **impossible à faire planter**, quoi que tape l'utilisateur. 1. Le programme affiche une invite `> ` et lit une expression de la forme `nombre opérateur nombre`, séparée par des espaces : `12 + 4`, `7.5 * 2`, `2 ** 10`. 2. Opérateurs acceptés : `+`, `-`, `*`, `/`, `//`, `%`, `**`. 3. Les nombres peuvent être décimaux, avec un point **ou** une virgule (`3,5`). 4. Le résultat est affiché ; s'il est entier (par exemple `4.0`), on l'affiche sans décimale (`4`). 5. Le mot-clé `ans` peut remplacer un nombre : il vaut le dernier résultat calculé (0 au départ). 6. Commandes spéciales : `historique` (affiche tous les calculs réussis), `aide`, `quitter`. 7. Toute erreur affiche un message clair commençant par `Erreur :` et le programme continue : - format incorrect (pas 3 éléments) ; - nombre invalide ; - opérateur inconnu ; - division par zéro ; - résultat trop grand (`10 ** 10000` en `float` → `OverflowError`). 8. **Ctrl+C** et **Ctrl+D** (fin de saisie) quittent proprement avec `Au revoir !`, sans traceback. 9. Le code définit une exception personnalisée `ExpressionInvalideError` pour les erreurs de format. Exemple de session : ``` Calculatrice robuste — tape 'aide' pour l'aide, 'quitter' pour sortir. > 12 + 4 = 16 > ans * 2,5 = 40 > 10 / 0 Erreur : division par zéro. > 3 $ 4 Erreur : opérateur inconnu '$'. > douze + 1 Erreur : 'douze' n'est pas un nombre. > 5 + Erreur : il faut 3 éléments séparés par des espaces (ex : 3 + 4). > 2.0 ** 10000 Erreur : résultat trop grand. > historique 1. 12 + 4 = 16 2. 16 * 2.5 = 40 > quitter Au revoir ! ``` ### Étapes suggérées 1. Écris `convertir_nombre(texte, dernier_resultat)` : gère `ans`, remplace `,` par `.`, appelle `float()` et transforme la `ValueError` en `ExpressionInvalideError` avec un message clair. 2. Écris `calculer(a, operateur, b)` : un dictionnaire ou un `match` qui associe chaque opérateur à son calcul. Lève `ExpressionInvalideError` si l'opérateur est inconnu. Laisse `ZeroDivisionError` et `OverflowError` se produire naturellement. 3. Écris `analyser(ligne, dernier_resultat)` qui découpe la ligne, vérifie qu'il y a 3 morceaux et renvoie `(a, operateur, b)`. 4. Écris `formater(nombre)` qui affiche `16` plutôt que `16.0`. 5. Écris la boucle principale avec **un seul** `try` qui attrape chaque exception séparément, avec le bon message. 6. Entoure le tout d'un `try` / `except (KeyboardInterrupt, EOFError)`. 7. Teste **toutes** les erreurs de la liste : c'est le cœur du projet ! ### Pistes d'amélioration - Accepter les expressions sans espaces (`12+4`) en analysant la chaîne caractère par caractère. - Ajouter des fonctions à un argument : `racine 16`, `abs -3`. - Sauvegarder l'historique dans un fichier JSON à la sortie et le recharger au démarrage (en gérant le fichier absent ou corrompu !). - Compter les erreurs par type et les afficher en quittant. --- ## Corrigés
Corrigé de l'exercice 1 ```python def demander_entier(message): while True: saisie = input(message) try: return int(saisie) # return sort à la fois de la boucle et de la fonction except ValueError: print("Ce n'est pas un nombre entier, recommence.") age = demander_entier("Ton âge : ") print(f"Tu as {age} ans.") ``` Le `return` à l'intérieur du `try` n'est atteint que si `int(saisie)` a réussi. En cas d'échec, on passe dans l'`except`, puis la boucle `while True` recommence. Remarque qu'on n'a pas besoin de `break` : `return` suffit.
Corrigé de l'exercice 2 ```python def division_sure(a, b): try: return a / b except ZeroDivisionError: return "Division par zéro impossible" print(division_sure(10, 4)) print(division_sure(3, 0)) ``` ``` 2.5 Division par zéro impossible ``` Dans un vrai programme, renvoyer tantôt un nombre, tantôt une chaîne n'est pas idéal (l'appelant doit vérifier le type). Une meilleure conception consiste souvent à laisser l'exception remonter, ou à renvoyer `None`.
Corrigé de l'exercice 3 1. **Quoi ?** Une `TypeError` : *can't multiply sequence by non-int of type 'float'*, « impossible de multiplier une séquence par un non-entier de type float ». Une « séquence » ici, c'est une chaîne. 2. **Où ?** Le bloc le plus bas : ligne 2, dans `prix_ttc`, sur `prix_ht * 1.2`. 3. **Pourquoi ?** En remontant : `prix_ttc` a été appelée ligne 7 avec `prix`, qui vient du dictionnaire `panier`. Le premier article (`stylo`) est passé, c'est donc un article suivant qui pose problème : `"cahier": "3.2"` est une **chaîne**, pas un nombre ! (Une chaîne multipliée par un entier fonctionne, `"ab" * 2` donne `"abab"`, mais pas par un `float`.) Correction : mettre `"cahier": 3.2` dans les données. Si les données viennent d'un fichier qu'on ne contrôle pas, on peut convertir dans `prix_ttc` : ```python def prix_ttc(prix_ht): return float(prix_ht) * 1.2 def afficher_facture(articles): for nom, prix in articles.items(): print(f"{nom} : {prix_ttc(prix):.2f} €") panier = {"stylo": 1.5, "cahier": "3.2", "gomme": 0.8} afficher_facture(panier) ``` ``` stylo : 1.80 € cahier : 3.84 € gomme : 0.96 € ``` Leçon : le crime a eu lieu ligne 2, mais le coupable était dans les **données** de la ligne 10.
Corrigé de l'exercice 4 ```python def convertir_notes(textes): notes_valides = [] rejets = [] for texte in textes: try: note = float(texte.replace(",", ".")) except ValueError: rejets.append(texte) continue # on passe directement à la chaîne suivante if 0 <= note <= 20: notes_valides.append(note) else: rejets.append(texte) return notes_valides, rejets valides, rejets = convertir_notes(["12", "15.5", "abc", "12,5", "25", "", "-1", "8"]) print(valides) print(rejets) ``` ``` [12.0, 15.5, 12.5, 8.0] ['abc', '25', '', '-1'] ``` Deux types de rejets : ceux qui ne sont pas des nombres (exception `ValueError`, y compris la chaîne vide), et ceux qui sont des nombres hors limites (simple `if`). Le `continue` évite d'utiliser une variable `note` qui n'existerait pas (voir le piège 8). On aurait aussi pu utiliser un bloc `else` sur le `try`.
Corrigé de l'exercice 5 ```python import json from pathlib import Path CONFIG_PAR_DEFAUT = {"langue": "fr", "volume": 50} def charger_config(chemin): chemin = Path(chemin) try: texte = chemin.read_text(encoding="utf-8") return json.loads(texte) except FileNotFoundError: print("Fichier absent, configuration par défaut.") except json.JSONDecodeError: print("Fichier corrompu, configuration par défaut.") return dict(CONFIG_PAR_DEFAUT) # une copie, pour ne pas modifier l'original # Tests Path("bonne.json").write_text('{"langue": "en", "volume": 80}', encoding="utf-8") Path("cassee.json").write_text('{"langue": "en", volume', encoding="utf-8") print(charger_config("bonne.json")) print(charger_config("absente.json")) print(charger_config("cassee.json")) ``` ``` {'langue': 'en', 'volume': 80} Fichier absent, configuration par défaut. {'langue': 'fr', 'volume': 50} Fichier corrompu, configuration par défaut. {'langue': 'fr', 'volume': 50} ``` On renvoie une **copie** (`dict(...)`) de la configuration par défaut : si l'appelant modifie le dictionnaire reçu, la constante globale reste intacte. `json.JSONDecodeError` est d'ailleurs une sous-classe de `ValueError`.
Corrigé de l'exercice 6 ```python def retirer_du_stock(stock, produit, quantite): disponible = stock[produit] # KeyError si le produit n'existe pas if quantite <= 0: raise ValueError(f"quantité invalide : {quantite}") if quantite > disponible: raise ValueError(f"stock insuffisant pour {produit} ({disponible} disponible(s))") stock[produit] = disponible - quantite stock = {"pomme": 10, "poire": 4, "kiwi": 0} demandes = [("pomme", 3), ("banane", 1), ("poire", 10), ("kiwi", -2), ("poire", 4)] for produit, quantite in demandes: try: retirer_du_stock(stock, produit, quantite) except KeyError: print(f"Erreur : produit inconnu {produit!r}") except ValueError as erreur: print(f"Erreur : {erreur}") else: print(f"OK : {quantite} {produit}(s) retirée(s)") print("Stock final :", stock) ``` ``` OK : 3 pomme(s) retirée(s) Erreur : produit inconnu 'banane' Erreur : stock insuffisant pour poire (4 disponible(s)) Erreur : quantité invalide : -2 OK : 4 poire(s) retirée(s) Stock final : {'pomme': 7, 'poire': 0, 'kiwi': 0} ``` La fonction ne fait **aucun** `print` : elle signale les problèmes par des exceptions, et c'est la boucle (l'appelant) qui décide comment les présenter. Le bloc `else` n'affiche « OK » que si aucune exception n'a eu lieu. `{produit!r}` affiche la chaîne avec ses guillemets.
Corrigé de l'exercice 7 ```python class MotDePasseInvalideError(Exception): pass def verifier_mot_de_passe(mdp): if len(mdp) < 8: raise MotDePasseInvalideError( f"au moins 8 caractères (actuellement {len(mdp)})" ) if not any(caractere.isdigit() for caractere in mdp): raise MotDePasseInvalideError("il faut au moins un chiffre") if not any(caractere.isupper() for caractere in mdp): raise MotDePasseInvalideError("il faut au moins une majuscule") return True while True: mdp = input("Mot de passe : ") try: verifier_mot_de_passe(mdp) except MotDePasseInvalideError as erreur: print(f"Refusé : {erreur}") else: print("Mot de passe accepté !") break ``` Avec les entrées `abc`, `abcdefgh`, `abcdefg1`, `Abcdefg1` : ``` Mot de passe : abc Refusé : au moins 8 caractères (actuellement 3) Mot de passe : abcdefgh Refusé : il faut au moins un chiffre Mot de passe : abcdefg1 Refusé : il faut au moins une majuscule Mot de passe : Abcdefg1 Mot de passe accepté ! ``` `any(...)` renvoie `True` si au moins un élément est vrai (on a vu ce genre d'expression avec les compréhensions au [chapitre 06](06-chaines-et-comprehensions.md)). Une version sans `any` utiliserait une boucle `for` avec une variable booléenne.
Corrigé de l'exercice 8 Les trois bugs : 1. **Plantage** : `top_mots(...)[3]` provoque `IndexError: list index out of range`, car la liste du top 3 a les indices 0, 1, 2. De plus, on veut le **mot** (premier élément de la paire), donc `[0][0]`. *Trouvé grâce au traceback.* 2. **Le compteur ne compte pas** : dans `compter_mots`, les deux branches font `= 1`. Il faut `compteur[mot] += 1` dans la première. *Trouvé avec un `print(compteur)` ou en observant `compteur` dans le panneau VARIABLES : tous les mots valaient 1.* 3. **Tri à l'envers** : `sorted` trie par ordre croissant, donc on obtient les mots les **moins** fréquents. Il faut `reverse=True`. *Trouvé en affichant `classement`.* ```python def compter_mots(texte): compteur = {} for mot in texte.split(): mot = mot.lower().strip(".,!?") if mot in compteur: compteur[mot] += 1 # bug 2 corrigé else: compteur[mot] = 1 return compteur def top_mots(compteur, n): classement = sorted(compteur.items(), key=lambda paire: paire[1], reverse=True) # bug 3 return classement[:n] texte = "Le chat dort. Le chien dort aussi. Le chat rêve !" for mot, nombre in top_mots(compter_mots(texte), 3): print(f"{mot} : {nombre}") print("Mot le plus fréquent :", top_mots(compter_mots(texte), 3)[0][0]) # bug 1 ``` ``` le : 3 chat : 2 dort : 2 Mot le plus fréquent : le ``` Attention à un détail : le mot `!` isolé à la fin devient la chaîne vide `""` après `strip`. Il n'apparaît pas dans le top 3 ici, mais une version plus propre ignorerait les mots vides avec `if not mot: continue`. Remarque aussi que `sorted` est **stable** : à égalité (chat et dort valent 2), l'ordre d'apparition est conservé.
Corrigé du mini-projet ```python """Calculatrice en ligne de commande qui ne plante jamais.""" class ExpressionInvalideError(Exception): pass OPERATEURS = ["+", "-", "*", "/", "//", "%", "**"] AIDE = """Saisis : nombre opérateur nombre (séparés par des espaces). Opérateurs : + - * / // % ** 'ans' = dernier résultat. Commandes : historique, aide, quitter.""" def convertir_nombre(texte, dernier_resultat): """Convertit un texte en float. Accepte 'ans' et la virgule décimale.""" if texte.lower() == "ans": return dernier_resultat try: return float(texte.replace(",", ".")) except ValueError: raise ExpressionInvalideError(f"{texte!r} n'est pas un nombre.") from None def calculer(a, operateur, b): """Effectue le calcul. Peut lever ZeroDivisionError ou OverflowError.""" match operateur: case "+": return a + b case "-": return a - b case "*": return a * b case "/": return a / b case "//": return a // b case "%": return a % b case "**": return a**b case _: raise ExpressionInvalideError(f"opérateur inconnu {operateur!r}.") def analyser(ligne, dernier_resultat): """Découpe la ligne en (a, operateur, b).""" morceaux = ligne.split() if len(morceaux) != 3: raise ExpressionInvalideError( "il faut 3 éléments séparés par des espaces (ex : 3 + 4)." ) texte_a, operateur, texte_b = morceaux a = convertir_nombre(texte_a, dernier_resultat) b = convertir_nombre(texte_b, dernier_resultat) return a, operateur, b def formater(nombre): """Affiche 16 plutôt que 16.0, et arrondit l'affichage des décimaux.""" if isinstance(nombre, complex): return str(nombre) if nombre.is_integer(): return str(int(nombre)) return f"{nombre:.10g}" def boucle_principale(): historique = [] dernier_resultat = 0.0 print("Calculatrice robuste — tape 'aide' pour l'aide, 'quitter' pour sortir.") while True: ligne = input("> ").strip() commande = ligne.lower() if commande == "": continue if commande == "quitter": break if commande == "aide": print(AIDE) continue if commande == "historique": if not historique: print("(historique vide)") for numero, calcul in enumerate(historique, start=1): print(f"{numero}. {calcul}") continue try: a, operateur, b = analyser(ligne, dernier_resultat) resultat = calculer(a, operateur, b) except ExpressionInvalideError as erreur: print(f"Erreur : {erreur}") except ZeroDivisionError: print("Erreur : division par zéro.") except OverflowError: print("Erreur : résultat trop grand.") else: dernier_resultat = resultat texte = f"{formater(a)} {operateur} {formater(b)} = {formater(resultat)}" historique.append(texte) print(f"= {formater(resultat)}") def main(): try: boucle_principale() except (KeyboardInterrupt, EOFError): print() # passe à la ligne après le ^C ou ^D print("Au revoir !") main() ``` Session de test (les saisies sont après `> `) : ``` Calculatrice robuste — tape 'aide' pour l'aide, 'quitter' pour sortir. > 12 + 4 = 16 > ans * 2,5 = 40 > 10 / 0 Erreur : division par zéro. > 3 $ 4 Erreur : opérateur inconnu '$'. > douze + 1 Erreur : 'douze' n'est pas un nombre. > 5 + Erreur : il faut 3 éléments séparés par des espaces (ex : 3 + 4). > 2.0 ** 10000 Erreur : résultat trop grand. > 7 // 2 = 3 > 0.1 + 0.2 = 0.3 > historique 1. 12 + 4 = 16 2. 16 * 2.5 = 40 3. 7 // 2 = 3 4. 0.1 + 0.2 = 0.3 > quitter Au revoir ! ``` Points importants : - **Une seule exception personnalisée** pour toutes les erreurs de saisie : la boucle principale n'a pas besoin de savoir *quel* contrôle a échoué, le message le dit. - `raise ... from None` dans `convertir_nombre` masque l'exception d'origine (`ValueError`) dans un éventuel traceback : on la remplace volontairement par la nôtre. - Le `try` principal ne contient que les deux appels risqués ; la mise à jour de l'historique est dans le `else`. - `-8 ** 0.5` (la puissance d'un nombre négatif par un décimal) donne un nombre **complexe** en Python (`(1.7319121124709868e-16+2.8284271247461903j)`) : `formater` le gère avec `isinstance` au lieu de planter sur `.is_integer()`. Ce genre de cas limite, on le découvre… en essayant de faire planter son programme ! - `f"{nombre:.10g}"` affiche au plus 10 chiffres significatifs : `0.1 + 0.2` s'affiche `0.3` au lieu de `0.30000000000000004`. - Ctrl+C (`KeyboardInterrupt`) et Ctrl+D / fin de fichier (`EOFError`) sont attrapés **une seule fois**, autour de toute la boucle.
--- ## Récapitulatif - Une **erreur de syntaxe** empêche le programme de démarrer ; une **exception** survient pendant l'exécution. - Un **traceback se lit de bas en haut** : dernière ligne = *quoi* ; dernier bloc `File` de ton fichier = *où* ; blocs au-dessus = *comment on en est arrivé là*. - Exceptions courantes : `ValueError` (mauvaise valeur), `TypeError` (mauvais type), `KeyError` (clé absente), `IndexError` (indice hors limites), `FileNotFoundError` (fichier introuvable), `ZeroDivisionError`, `NameError`, `AttributeError`. Elles forment une **hiérarchie** : attraper un parent attrape ses enfants. - `try` / `except Type` intercepte une erreur précise ; `as erreur` récupère l'objet et son message ; `else` s'exécute si tout s'est bien passé ; `finally` s'exécute toujours. - **Jamais de `except:` nu** ; garde le bloc `try` le plus court possible ; exceptions précises d'abord. - `raise ValueError("message")` signale un problème et laisse l'appelant décider ; `raise` seul relance ; `raise ... from ...` chaîne les erreurs. - Créer une exception : `class MonErreur(Exception): pass` (les classes seront expliquées au chapitre 10). - Déboguer : `print(f"{variable=}")`, `breakpoint()` (commandes `p`, `n`, `s`, `l`, `c`, `q`), débogueur VS Code (point d'arrêt F9, lancement F5, pas à pas F10 / F11, panneaux Variables / Watch / Call Stack). - `assert condition, "message"` vérifie tes hypothèses de programmeur et sert aux tests, **pas** à valider des saisies. ### Checklist de validation - [ ] Je sais trouver en moins de 30 secondes le type d'erreur et la ligne fautive dans un traceback. - [ ] Je sais citer et expliquer au moins six exceptions courantes. - [ ] Je sais écrire une boucle qui redemande une saisie jusqu'à ce qu'elle soit valide. - [ ] Je sais à quoi servent `else` et `finally` dans un `try`. - [ ] Je sais lever une exception avec `raise` et créer ma propre exception. - [ ] J'ai posé un point d'arrêt dans VS Code et avancé pas à pas en observant les variables. - [ ] Je sais pourquoi `except:` tout seul est dangereux et pourquoi `assert` ne doit pas valider une saisie. - [ ] Ma calculatrice résiste à toutes les saisies que j'ai pu imaginer, y compris Ctrl+C. --- [← Chapitre précédent](07-fichiers-csv-json.md) | [Sommaire](README.md) | [Chapitre suivant →](09-modules-bibliotheque-standard.md)