⚠️ Le cours : les exceptions
Quand quelque chose d'anormal se produit pendant l'exécution (division par zéro, fichier introuvable, conversion impossible…), Python lève une exception : il interrompt le déroulement normal et remonte les appels de fonctions à la recherche d'un code qui sait la traiter. Si personne ne l'intercepte, le programme s'arrête en affichant une trace (*traceback*) : la liste des appels qui ont mené à l'erreur, et, à la fin, le type et le message de l'exception.
def diviser(a, b):
return a / b
print(diviser(6, 3))
print(diviser(1, 0))
Ce qui s'affiche2.0
Traceback (most recent call last):
File "trace.py", line 5, in <module>
print(diviser(1, 0))
File "trace.py", line 2, in diviser
return a / b
ZeroDivisionError: division by zero
Pour lire une trace, on commence par la dernière ligne (le type et le message de l'erreur), puis on remonte pour trouver la ligne de ton code qui l'a provoquée.
Les exceptions les plus courantes
| Exception | Quand ? | Exemple |
ValueError | la valeur a le bon type mais un contenu invalide | int("abc") |
TypeError | opération sur des types incompatibles | "a" + 1 len(5) |
IndexError | indice hors d'une séquence | [1, 2][5] |
KeyError | clé absente d'un dictionnaire | {"a": 1}["b"] |
ZeroDivisionError | division ou modulo par zéro | 1 / 0 5 % 0 |
NameError | nom de variable inconnu | print(x) sans avoir défini x |
AttributeError | attribut ou méthode inexistant | "a".ajouter(1) |
FileNotFoundError | fichier introuvable | open("absent.txt") |
ImportError ModuleNotFoundError | module introuvable | import modulequinexistepas |
RecursionError | trop d'appels récursifs imbriqués | fonction récursive sans cas de base |
erreurs = [
lambda: int("abc"),
lambda: "a" + 1,
lambda: [1, 2][5],
lambda: {"a": 1}["b"],
lambda: 1 / 0,
lambda: "a".ajouter(1),
]
for provoquer in erreurs:
try:
provoquer()
except Exception as e:
print(type(e).__name__)
AffichageValueError
TypeError
IndexError
KeyError
ZeroDivisionError
AttributeError
try et except : intercepter une exception
On place le code à risque dans un bloc try. Si une exception se produit, l'exécution saute au premier bloc except dont le type correspond ; sinon, les blocs except sont ignorés. Le programme continue ensuite normalement après le try. Avec as e, on récupère l'objet exception : str(e) donne son message.
def lire_entier(texte):
try:
return int(texte)
except ValueError as e:
print("erreur :", e)
return None
print(lire_entier("42"))
print(lire_entier("4x2"))
Affichage42
erreur : invalid literal for int() with base 10: '4x2'
None
Plusieurs except : du plus précis au plus général
On peut enchaîner plusieurs blocs except pour réagir différemment selon le type, ou regrouper plusieurs types dans un tuple. Python examine les blocs dans l'ordre : il faut donc placer les exceptions précises avant les générales (Exception capture presque tout). Une except trop large peut cacher des erreurs qu'on ne voulait pas traiter.
def calculer(a, b):
try:
return a / b
except ZeroDivisionError:
return "division par zéro"
except (TypeError, ValueError):
return "valeurs invalides"
print(calculer(6, 3))
print(calculer(1, 0))
print(calculer("1", 2))
Affichage2.0
division par zéro
valeurs invalides
else et finally
Le bloc else s'exécute seulement si le try s'est terminé sans exception : il contient la suite du code qui dépend du succès, sans qu'une erreur de cette suite soit interceptée par erreur. Le bloc finally s'exécute dans tous les cas (succès, exception interceptée, exception non interceptée, return) : c'est l'endroit pour libérer une ressource.
def diviser(a, b):
try:
resultat = a / b
except ZeroDivisionError:
print("division par zéro")
else:
print("résultat :", resultat)
finally:
print("fin de l'opération")
diviser(6, 3)
diviser(1, 0)
Affichagerésultat : 2.0
fin de l'opération
division par zéro
fin de l'opération
raise : signaler une erreur
raise ValueError("message") lève volontairement une exception : une fonction l'utilise pour refuser des valeurs invalides au lieu de renvoyer un résultat absurde. Un raise seul, dans un bloc except, relance l'exception en cours (après l'avoir, par exemple, journalisée). raise NouvelleErreur(...) from e enchaîne deux erreurs en gardant la cause d'origine.
def racine_entiere(n):
if n < 0:
raise ValueError("n doit être positif")
return int(n ** 0.5)
print(racine_entiere(49))
try:
racine_entiere(-4)
except ValueError as e:
print("Erreur :", e)
def lire(texte):
try:
return int(texte)
except ValueError as e:
raise RuntimeError("lecture impossible") from e
try:
lire("x")
except RuntimeError as e:
print(e, "| cause :", type(e.__cause__).__name__)
Affichage7
Erreur : n doit être positif
lecture impossible | cause : ValueError
Créer ses propres exceptions
Une exception personnalisée est une classe qui hérite d'Exception (ou d'une de ses filles). Elle donne un nom précis à une erreur du domaine du programme (SoldeInsuffisantError) et peut transporter des informations dans ses attributs.
class SoldeInsuffisantError(Exception):
def __init__(self, solde, montant):
super().__init__(f"solde {solde} < montant {montant}")
self.manque = montant - solde
def retirer(solde, montant):
if montant > solde:
raise SoldeInsuffisantError(solde, montant)
return solde - montant
print(retirer(100, 30))
try:
retirer(50, 80)
except SoldeInsuffisantError as e:
print(e, "| manque :", e.manque)
Affichage70
solde 50 < montant 80 | manque : 30
La hiérarchie des exceptions
Les exceptions forment une hiérarchie : intercepter une classe intercepte aussi toutes ses filles. ArithmeticError regroupe ZeroDivisionError et OverflowError ; LookupError regroupe IndexError et KeyError ; Exception est la mère de presque toutes les erreurs « normales ».
BaseException
├── SystemExit, KeyboardInterrupt (arrêt du programme : on ne les intercepte pas)
└── Exception
├── ArithmeticError ─ ZeroDivisionError, OverflowError
├── LookupError ─ IndexError, KeyError
├── OSError ─ FileNotFoundError, PermissionError
├── ValueError
├── TypeError
├── NameError
├── AttributeError
├── ImportError ─ ModuleNotFoundError
├── RuntimeError ─ RecursionError, NotImplementedError
└── AssertionError
print(issubclass(ZeroDivisionError, ArithmeticError))
print(issubclass(KeyError, LookupError), issubclass(KeyError, ValueError))
try:
{"a": 1}["b"]
except LookupError as e:
print("capturée par LookupError :", type(e).__name__)
AffichageTrue
True False
capturée par LookupError : KeyError
with : fermer proprement une ressource
L'instruction with utilise un gestionnaire de contexte : elle garantit que la ressource (fichier, connexion) est libérée à la sortie du bloc, même si une exception s'y produit. C'est l'équivalent d'un try / finally écrit pour toi.
try:
with open("inexistant.txt") as f:
contenu = f.read()
except FileNotFoundError:
print("fichier introuvable")
with open("ok.txt", "w") as f:
f.write("bonjour")
print(f.closed)
Affichagefichier introuvable
True
assert : vérifier ce qu'on croit vrai
assert condition, "message" lève une AssertionError si la condition est fausse. On s'en sert pour tester une fonction et pour documenter une hypothèse du programme ; ce n'est pas un moyen de valider des saisies d'utilisateur (les assert peuvent être désactivés avec l'option -O).
def moyenne(notes):
assert len(notes) > 0, "la liste est vide"
return sum(notes) / len(notes)
print(moyenne([12, 15]))
try:
moyenne([])
except AssertionError as e:
print("AssertionError :", e)
Affichage13.5
AssertionError : la liste est vide
Demander pardon ou demander la permission ?
Il y a deux styles. LBYL (*look before you leap*) : on teste avant d'agir (if cle in d:). EAFP (*easier to ask forgiveness than permission*) : on essaie, et on traite l'erreur si elle survient. Python encourage souvent l'EAFP, surtout quand le test préalable est compliqué ou peu sûr (un fichier peut disparaître entre le test et l'ouverture).
stock = {"pommes": 4, "poires": 0}
def disponible_lbyl(produit):
return produit in stock and stock[produit] > 0
def disponible_eafp(produit):
try:
return stock[produit] > 0
except KeyError:
return False
print(disponible_lbyl("kiwis"), disponible_eafp("kiwis"), disponible_eafp("pommes"))
AffichageFalse False True
Réessayer ou ignorer une valeur invalide
Dans une boucle, try / except permet de continuer malgré une donnée invalide, ou de redemander une valeur jusqu'à ce qu'elle convienne.
valeurs = ["12", "x", "7", "", "3"]
total = 0
ignorees = 0
for v in valeurs:
try:
total += int(v)
except ValueError:
ignorees += 1
print(total, ignorees)
saisies = iter(["abc", "-3", "25"])
while True:
try:
age = int(next(saisies))
if age < 0:
raise ValueError("âge négatif")
break
except ValueError as e:
print("refusé :", e)
print("âge accepté :", age)
Affichage22 2
refusé : invalid literal for int() with base 10: 'abc'
refusé : âge négatif
âge accepté : 25