Programmation structurée (Python) · L1 · Section 6/11
Exceptions
Progression
#Exceptions
Une exception signale une situation anormale sans interrompre brutalement le programme: la fonction courante s'arrête, et l'erreur remonte à l'appelant jusqu'à trouver un except adapté. C'est le mécanisme qui sépare le cas normal du traitement des erreurs.
Prérequis: fonctions, fichiers.
Objectifs d'apprentissage:
- Écrire des blocs
try/except/else/finallyselon l'intention: gérer une erreur ou libérer une ressource. - Intercepter des exceptions précises et en lever avec des messages utiles.
- Chaîner les exceptions avec
raise ... frompour préserver la cause. - Utiliser des context managers pour automatiser le nettoyage.
#Anatomie d'un bloc try
1def lire_entier(invite: str) -> int:2 while True:3 reponse = input(invite)4 try:5 return int(reponse)6 except ValueError:7 print(f"'{reponse}' n'est pas un entier, réessayez.")8 9# int('abc') lève ValueError, interceptée ici: la boucle redemande.Les quatre clauses, dans l'ordre:
try: le code surveillé.except TypeErreur as e: traitement si cette erreur survient (plusieurs clauses possibles, de la plus précise à la plus générale).else: exécuté si aucune exception n'est survenue (sépare le cas de succès du code surveillé).finally: toujours exécuté, succès ou échec (libération de ressources).
1try:2 valeur = 10 / 23except ZeroDivisionError:4 print('division par zéro')5else:6 print('résultat:', valeur) # résultat: 5.07finally:8 print('toujours exécuté')9# Sortie:10# résultat: 5.011# toujours exécuté#Lever une exception
Une fonction qui détecte une entrée invalide le signale avec raise plutôt que de retourner une valeur magique:
1def racine(n: float) -> float:2 if n < 0:3 raise ValueError(f"n doit être >= 0, reçu {n}")4 return n ** 0.55 6print(racine(9)) # 3.07 8try:9 racine(-1)10except ValueError as e:11 print('Refusé:', e)12# Sortie: Refusé: n doit être >= 0, reçu -1Le message inclut la valeur fautive: c'est ce qui fait gagner du temps au débogage. Retourner None, -1 ou 0 en cas d'erreur est un anti-pattern: l'appelant peut ignorer la valeur et le bug se propage silencieusement.
#Hiérarchie et exceptions métier
Les exceptions forment une hiérarchie: ZeroDivisionError et ValueError héritent d'Exception. Créer sa propre classe héritant d'Exception distingue les erreurs métier des erreurs système:
1class SoldeInsuffisant(Exception):2 """Erreur métier: retrait au-delà du solde disponible."""3 4def retire(solde: float, montant: float) -> float:5 if montant > solde:6 raise SoldeInsuffisant(f"solde {solde}, retrait demandé {montant}")7 return solde - montant8 9try:10 retire(100, 150)11except SoldeInsuffisant as e:12 print('Opération refusée:', e)13# Sortie: Opération refusée: solde 100, retrait demandé 150#Chaînage: raise ... from
Envelopper une erreur technique dans une erreur métier sans perdre la cause:
1class ErreurConfig(Exception):2 pass3 4def charge_config(chemin: str) -> dict:5 try:6 with open(chemin, encoding='utf-8') as f:7 import json8 return json.load(f)9 except OSError as e:10 raise ErreurConfig(f"impossible de lire {chemin}") from e11 12try:13 charge_config('absent.json')14except ErreurConfig as e:e.__cause__ conserve l'erreur d'origine: la trace complète reste consultable au débogage.
#Nettoyage: finally et context managers
finally convient quand il n'y a pas d'abstraction disponible; le context manager with exprime la même garantie de libération de façon déclarative:
1from contextlib import contextmanager2import time3 4@contextmanager5def chronometre():6 debut = time.perf_counter()7 try:8 yield9 finally:10 duree_ms = (time.perf_counter() - debut) * 100011 print(f"durée: {duree_ms:.1f} ms")12 13with chronometre():14 total = sum(range(100_000))Le code après yield s'exécute à la sortie du bloc, normalement ou sur exception. C'est exactement le mécanisme de with open(...) as f: pour refermer le fichier.
#Playground
#Exercices
- Parseur robuste. Lisez des lignes au format
nom;note(certaines invalides: note non numérique, nombre de champs incorrect). Affichez les paires valides et un rapport listant chaque ligne invalide avec sa raison. Testez sur['Ada;18', 'Bob;abc', 'Eve']. - Chronomètre vérifié. Reprenez
chronometreci-dessus et vérifiez qu'il affiche bien la durée même quand le bloc lève une exception (levezRuntimeErrordans lewithet interceptez-le autour). - Retrait bancaire. À partir de
retire, écrivez une boucle qui traite une liste d'opérations, ignore celles qui échouent avecSoldeInsuffisanten les signalant, et affiche le solde final. Départ 100, opérations[('retrait', 30), ('retrait', 90), ('retrait', 20)], attendu: solde final 50 et un refus.
#Corrections
Correction: parseur robuste
1lignes = ['Ada;18', 'Bob;abc', 'Eve']2valides, erreurs = [], []3 4for numero, ligne in enumerate(lignes, start=1):5 parties = ligne.strip().split(';')6 if len(parties) != 2:7 erreurs.append((numero, 'nombre de champs incorrect'))8 continue9 nom, note = parties10 try:11 valides.append((nom, int(note)))12 except ValueError:13 erreurs.append((numero, f"note non entière: {note!r}"))14 Le rapport d'erreurs porte le numéro de ligne et la raison: c'est lui qui rend le traitement auditable. Intercepter ValueError au plus près de la conversion garde le reste de la boucle sain.
Correction: chronomètre vérifié
1from contextlib import contextmanager2import time3 4@contextmanager5def chronometre():6 debut = time.perf_counter()7 try:8 yield9 finally:10 print(f"durée: {(time.perf_counter() - debut) * 1000:.1f} ms")11 12try:13 with chronometre():14 raise RuntimeError('panne simulée')La ligne "durée" s'affiche avant "interceptée": finally s'exécute pendant la remontée de l'exception, avant que le except externe ne l'attrape. La garantie tient même en cas d'erreur.
Correction: retrait bancaire
1class SoldeInsuffisant(Exception):2 pass3 4def retire(solde: float, montant: float) -> float:5 if montant > solde:6 raise SoldeInsuffisant(f"solde {solde}, retrait {montant}")7 return solde - montant8 9solde = 10010for _, montant in [('retrait', 30), ('retrait', 90), ('retrait', 20)]:11 try:12 solde = retire(solde, montant)13 except SoldeInsuffisant as e:14 print('refus:', e)L'exception interceptée permet de poursuivre le traitement: une opération invalide n'interrompt pas la file d'opérations.
#Pièges fréquents
#return dans finally supprime l'exception
1def f():2 try:3 1 / 04 finally:5 return 'masque l\'exception' # à ne jamais faire6 7print(f()) # 'masque l'exception': le ZeroDivisionError a disparuUn return (ou break) dans finally avale l'exception en cours de propagation. Règle: finally nettoie, il ne retourne rien.
#except trop large
except Exception: sans re-raise ciblé intercepte aussi les fautes de frappe (NameError, AttributeError) et transforme un bug évident en comportement silencieux erroné. Interceptez le type précis; si vous devez intercepter large, journalisez puis relancez avec raise.
#Exception silencieuse
1try:2 traite(fichier)3except Exception:4 pass # anti-pattern: l'erreur existe mais personne ne la voitMême en production, journalisez l'erreur. Un pass rend le programme impossible à déboguer.