Exceptions — gérer ce qui casse
Attraper une erreur précise avec try/except, pourquoi `except Exception: pass` est presque toujours faux, `else` et `finally`, lever les vôtres, et choisir une erreur de la bonne taille dans la famille.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
La dernière question du chapitre précédent faisait traiter correctement trois lignes à un programme, puis s'arrêter à la quatrième. En version réduite, voici à quoi cela ressemble :
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
total += int(text)
print(total)ValueError: invalid literal for int() with base 10: 'twenty'(Avant la ligne d'erreur, Python imprime aussi quelques lignes de traceback ; dans ce chapitre, seule la dernière ligne est en général montrée.)
10 a été ajouté correctement. 30 n'a jamais été ajouté du tout. Et print(total) ne s'est jamais exécuté.
Une seule mauvaise ligne a arrêté tout le travail. Jusqu'ici, nous n'avions que deux options — soit chaque donnée est parfaite, soit le programme s'arrête.
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except ValueError:
print("skipping", text)
print(total)skipping twenty
40Il existe une troisième option : laisser le problème se produire, puis y répondre.
À la fin de ce chapitre vous pourrez
- Attraper une erreur précise avec
try/except - Expliquer pourquoi un
except:nu ou unexcept Exception: passest presque toujours faux - Saisir l'erreur avec
as erret lire son message - Dire ce que font
elseetfinally raisevos propres erreurs, et juger quand le faire- Reconnaître la famille des erreurs, et en attraper une de la bonne taille
Prérequis : Lecture et écriture de fichiers.
Une erreur est aussi un objet
Écrire as met l'erreur entre vos mains :
try:
int("twenty")
except ValueError as err:
print(type(err))
print(err)<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'Le texte rouge que vous voyez dans le terminal depuis tout ce temps, ce sont ces objets, et en imprimer un donne son message.
Il y a une conséquence pratique : l'erreur porte la meilleure explication qui soit. Imprimer err est presque toujours plus utile que d'écrire « quelque chose s'est mal passé » avec vos propres mots.
Attraper la bonne erreur
try:
int("twenty")
except TypeError:
print("caught")ValueError: invalid literal for int() with base 10: 'twenty'Un except n'attrape que la sorte qu'il nomme. Celui-ci nomme TypeError, et ce qui s'est produit est un ValueError — le bloc except peut aussi bien ne pas être là.
Cela peut sembler bloquant, et c'est justement ce qui est utile. La ligne except dit quelle erreur vous étiez prêt à gérer — et tout ce pour quoi vous n'étiez pas prêt arrête encore le programme, ce qui est correct.
C'est pourquoi vous ne devriez pas écrire ceci :
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except Exception:
pass
print(total)40La réponse est juste et le code reste mauvais. except Exception: pass avale tout — les échecs auxquels vous avez pensé et ceux auxquels vous n'avez pas pensé. Quand numbers deviendra par accident un dictionnaire demain, ou qu'un nom à l'intérieur sera mal orthographié, ce programme donnera silencieusement une mauvaise réponse.
La règle : n'attraper que l'erreur pour laquelle vous avez un plan. Et une fois attrapée, faites quelque chose — au minimum, dites ce qui s'est passé.
Plus d'une sorte
def get(data, key):
try:
return data[key]
except KeyError:
return "no such key"
except IndexError:
return "no such position"
print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))15
no such key
no such positionPlusieurs clauses except peuvent se suivre, et la première qui correspond s'exécute — les autres ne sont même pas regardées.
Quand la réponse est la même, elles peuvent être écrites ensemble :
for value in ["10", None, "x"]:
try:
print(int(value))
except (ValueError, TypeError) as err:
print(type(err).__name__, "-", err)10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'type(err).__name__ donne le nom de la sorte d'erreur — utile quand on écrit des logs.
Les erreurs viennent par familles
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))True
True
True
TrueLes erreurs sont organisées en arbre, et attraper un parent attrape ses enfants :
try:
raise FileNotFoundError("gone")
except OSError as err:
print("caught as OSError:", err)caught as OSError: goneQuelques relations qu'il vaut la peine de connaître : FileNotFoundError et PermissionError sont tous les deux des OSError ; KeyError et IndexError sont tous les deux des LookupError ; et presque tout est un Exception.
D'où la règle : attraper la plus petite erreur possible. except Exception est le plus facile à écrire et le moins utile.
else et finally
def read(text):
try:
value = int(text)
except ValueError:
print("bad:", text)
return None
else:
print("good:", value)
return value
finally:
print("done with", text)
print(read("10"))
print(read("ten"))good: 10
done with 10
10
bad: ten
done with ten
Noneelse s'exécute quand rien dans le bloc try n'a cassé. Cela permet à l'intérieur de try de rester petit — seulement la ligne qui peut échouer — et le travail qui suit le succès va dans else. Mettez trop de choses dans try et vous attraperez des choses que vous ne vouliez pas attraper.
finally s'exécute toujours, quoi qu'il arrive. Même après un return :
def risky():
try:
return "from try"
finally:
print("finally still runs")
print(risky())finally still runs
from tryLe return a fixé la valeur, mais finally s'exécute avant de quitter la fonction. C'est l'endroit prévu pour le nettoyage — même si, pour les fichiers, with a déjà fait ce travail.
Lever vos propres erreurs
Lever une erreur compte autant qu'en attraper une.
def line_total(price, quantity):
if quantity < 0:
raise ValueError(f"quantity cannot be negative: {quantity}")
return price * quantity
print(line_total(15.0, 3))
print(line_total(15.0, -1))45.0
ValueError: quantity cannot be negative: -1Quelle était l'alternative à raise ? Renvoyer None, ou 0 — et dans ce cas le problème ferait son chemin. Il se glisserait dans un total et apparaîtrait bien plus tard comme un nombre étrange dont on ne peut pas tracer l'origine.
Une erreur arrête le problème là où il est né, et dit pourquoi dans son message.
En écrivant ce message, mettez-y la valeur fautive. "invalid quantity" et "quantity cannot be negative: -1" — seul le second vous permet de commencer à travailler.
Une erreur peut aussi être attrapée et relancée avec un meilleur message. parse.py :
def parse(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}")
print(parse("10"))
print(parse("ten"))10
Traceback (most recent call last):
File ".../parse.py", line 3, in parse
return int(text)
^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'ten'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../parse.py", line 9, in <module>
print(parse("ten"))
^^^^^^^^^^^^
File ".../parse.py", line 5, in parse
raise ValueError(f"not a number: {text!r}")
ValueError: not a number: 'ten'Cette fois, toute la sortie est montrée, parce qu'ici la dernière ligne ne raconte pas tout. (Sur votre machine, les lignes File contiendront le chemin complet du fichier ; ici, il est raccourci en ....) Python a imprimé deux tracebacks : le premier est l'erreur originale venant de int(), le second est notre nouveau ValueError. La ligne du milieu, During handling of the above exception, another exception occurred:, dit que le second s'est produit pendant qu'on traitait le premier. Aucune information n'est perdue — mais on lit cela comme s'il y avait un bug dans le bloc except.
Quand vous levez une nouvelle erreur exprès, dites-le : écrivez except ValueError as err: puis raise ValueError(f"not a number: {text!r}") from err. La ligne du milieu devient alors The above exception was the direct cause of the following exception: — c'est-à-dire « la première a causé la seconde », ce qui est exactement ce que nous voulons dire.
{text!r} insère la valeur via repr, donc les guillemets apparaissent — et la différence entre not a number: 'ten' et not a number: ten compte quand la valeur est vraiment une chaîne vide ou un espace parasite.
Demander d'abord, ou s'excuser ensuite
prices = {"pen": 15}
if "bag" in prices:
print(prices["bag"])
else:
print("not stocked")
try:
print(prices["bag"])
except KeyError:
print("not stocked")not stocked
not stockedLes deux sont justes. Le second est plus courant en Python, et pour une raison : le premier regarde deux fois — une fois dans in, une fois dans [...] — et peut encore casser si quelque chose change entre les deux.
Pour ce qui arrive régulièrement, un if se lit mieux. Les exceptions sont faites pour l'exceptionnel ; le nom le dit.
Un exemple complet
orders.txt, avec deux lignes exprès mauvaises :
pen,15.0,3
bag,eight,1
ink,120.0,2
clip,5.0main.py :
"""Read an order file, and keep going when a line is wrong."""
from pathlib import Path
TAX_RATE = 0.15
def parse_line(raw, number):
"""Returns one order line, or raises ValueError saying which line was wrong."""
parts = raw.split(",")
if len(parts) != 3:
raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")
name, price, quantity = parts
try:
return name, float(price), int(quantity)
except ValueError as err:
raise ValueError(f"line {number}: {err}")
def read_order(path):
"""Returns the good lines and the complaints, never raising for bad data."""
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return [], [f"no such file: {path}"]
lines = []
problems = []
for number, raw in enumerate(text.splitlines(), start=1):
if not raw.strip():
continue
try:
lines.append(parse_line(raw, number))
except ValueError as err:
problems.append(str(err))
return lines, problems
def main():
lines, problems = read_order(Path("orders.txt"))
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
print(f"{name:<6} {amount:>9.2f}")
print(f"{'total':<6} {total:>9.2f}")
print()
print(f"{len(lines)} lines used, {len(problems)} skipped")
for problem in problems:
print(" -", problem)
if __name__ == "__main__":
main()pen 51.75
ink 276.00
total 327.75
2 lines used, 2 skipped
- line 2: could not convert string to float: 'eight'
- line 5: expected 3 fields, got 2Quatre choses qui valent le coup d'être regardées.
parse_line lève et read_order attrape. La fonction du bas n'a aucune idée de ce que le programme dans son ensemble devrait faire — elle sait seulement que cette ligne est inutilisable. Décider quoi en faire appartient à la fonction du dessus. Les erreurs sont levées en bas et attrapées en haut, et cette répartition est tout le travail d'un système d'exceptions.
Le numéro de ligne est dans le message. could not convert string to float: 'eight' ne dit pas où chercher ; line 2: ... le dit. Tout ce que vous savez au moment d'attraper et que vous ne saurez plus ensuite appartient au message.
read_order ne casse jamais. Elle renvoie deux choses — ce qui était utilisable et ce qui ne l'était pas. Un fichier manquant produit une réponse de la même forme, donc main n'a pas besoin de deux chemins séparés.
Le bloc try intérieur fait exactement une ligne. float(price) et int(quantity) — seule la partie qui peut échouer est à l'intérieur. parts = raw.split(",") est à l'extérieur, parce qu'il ne devrait pas échouer, et s'il échoue je veux le savoir.
Quand ça casse
L'erreur n'est pas attrapée, alors qu'un except est là Le type ne correspond pas. Le premier mot de la dernière ligne du message est le nom de l'erreur — nommez celui-là, ou attrapez son parent.
Il y a un except et le programme s'arrête quand même La ligne qui échoue n'est pas à l'intérieur du bloc try. Vérifiez l'indentation.
SyntaxError: expected 'except' or 'finally' block Un try a été écrit sans except. try ne peut pas se tenir tout seul.
Tout « marche » mais les réponses sont fausses Il y a un except Exception: pass quelque part. Trouvez-le — c'est là que se trouve le problème.
J'ai levé quelque chose mais aucun message n'apparaît raise ValueError a été écrit au lieu de raise ValueError("..."). Ajoutez les parenthèses et le message.
Un return dans finally fait perdre l'erreur C'est le cas, et c'est pourquoi vous ne devriez pas en écrire un là. finally ne sert qu'au nettoyage.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Il y a un except. Que devient le programme ?
try:
int("twenty")
except TypeError:
print("caught")- A`caught` s'imprime
- BRien ne s'imprime et le programme se termine en silence
- CIl s'arrête avec un `TypeError`
- DLe programme s'arrête avec un `ValueError` — le `except` peut aussi bien ne pas être là
Toute la boucle est à l'intérieur du try. Qu'est-ce qui s'imprime ?
values = ["10", "x", "30"]
total = 0
try:
for text in values:
total += int(text)
except ValueError:
print("gave up")
print(total)- Agave up 40
- Bgave up 10
- C40
- Dgave up 0
Le try et le except ont tous les deux un return. Qu'est-ce qui s'imprime ?
def read(text):
try:
return int(text)
except ValueError:
return 0
finally:
print("checked", text)
print(read("7"))
print(read("seven"))- Achecked 7 7 checked seven 0
- B7 0
- Cchecked 7 7 0
- D7 checked 7 0 checked seven
Répondre nécessite un compte
Connectez-vous pour vérifier vos réponses
Les questions sont ci-dessus, et les résoudre de tête est ce qui compte vraiment. Connectez-vous pour voir les réponses, les explications et les indices à trois niveaux.
À vous de jouer
Retournez au summary.py du chapitre précédent, qui lisait people.txt.
Faites-en sorte qu'il ne plante jamais, et qu'il termine en disant quelles lignes ont été sautées et pourquoi. Écrivez un parse_line(raw, number) qui lève un ValueError contenant le numéro de ligne pour une mauvaise ligne.
Puis endommagez le fichier exprès — enlevez une virgule d'une ligne, mettez des lettres là où un nombre devrait se trouver dans une autre, et laissez une ligne entièrement vide. Par exemple :
rafi,12
nadia 30
sami,ten
lina,25La ligne vide n'est pas une plainte — comme dans le chapitre précédent, elle est sautée en silence. Le programme doit donc aller jusqu'au bout et annoncer deux plaintes. Si vous écrivez les messages comme dans l'exemple complet (expected 2 fields, got ... et line {number}: {err}), la fin de la sortie sera exactement ceci :
2 lines used, 2 skipped
- line 2: expected 2 fields, got 1
- line 3: invalid literal for int() with base 10: 'ten'Puis quatre expériences :
- Remplacez
except ValueErrorparexcept Exception: pass. Quelle est la sortie, et pourquoi est-elle plus dangereuse ? - Enlevez le numéro de ligne du message de
parse_line. Combien de temps faut-il maintenant pour trouver une mauvaise ligne ? - Agrandissez le bloc
try— mettez tout le corps de la boucle à l'intérieur. Que se passe-t-il maintenant quand une ligne est mauvaise ? - Ajoutez un
finallyqui imprime quelque chose après chaque ligne, et vérifiez qu'il s'exécute sur les bonnes lignes comme sur les mauvaises.
La troisième expérience enseigne l'habitude la plus utile de ce chapitre : plus le bloc try est petit, plus ce que vous attrapez est honnête.
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Connectez-vous pour faire le quiz