22. Débogage et gestion des erreurs#
« Le débogage est deux fois plus difficile que l’écriture du code initial. Par conséquent, si vous écrivez le code aussi astucieusement que possible, vous n’êtes, par définition, pas assez intelligent pour le déboguer. » – Brian Kernighan
22.1. Vue d’ensemble#
Êtes-vous l’un de ces programmeurs qui remplissent leur code d’instructions print lorsqu’ils essaient de déboguer leurs programmes ?
Bon, nous avons tous fait cela autrefois.
(D’accord, il nous arrive encore de le faire…)
Mais dès que vous commencerez à écrire des programmes plus volumineux, vous aurez besoin d’un meilleur système.
Vous voudrez peut-être aussi gérer les erreurs potentielles de votre code au moment où elles se produisent.
Dans ce cours, nous verrons comment déboguer nos programmes et améliorer la gestion des erreurs.
22.2. Débogage#
Les outils de débogage pour Python varient selon les plateformes, les IDE et les éditeurs.
Par exemple, un débogueur visuel est disponible dans JupyterLab.
Ici, nous nous concentrerons sur Jupyter Notebook et vous laisserons explorer les autres configurations.
Nous aurons besoin des imports suivants
import numpy as np
import matplotlib.pyplot as plt
22.2.1. La commande magique debug#
Considérons un exemple simple (et plutôt artificiel)
def plot_log():
fig, ax = plt.subplots(2, 1)
x = np.linspace(1, 2, 10)
ax.plot(x, np.log(x))
plt.show()
plot_log() # Appelle la fonction, génère le graphique
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[2], line 7
3 x = np.linspace(1, 2, 10)
4 ax.plot(x, np.log(x))
5 plt.show()
6
----> 7 plot_log() # Appelle la fonction, génère le graphique
Cell In[2], line 4, in plot_log()
1 def plot_log():
2 fig, ax = plt.subplots(2, 1)
3 x = np.linspace(1, 2, 10)
----> 4 ax.plot(x, np.log(x))
5 plt.show()
AttributeError: 'numpy.ndarray' object has no attribute 'plot'
Ce code est censé tracer la fonction log sur l’intervalle \([1, 2]\).
Mais il y a une erreur ici : plt.subplots(2, 1) devrait être simplement plt.subplots().
(L’appel plt.subplots(2, 1) renvoie un tableau NumPy contenant deux objets axes, adapté pour avoir deux sous-graphiques sur la même figure)
La trace d’exécution montre que l’erreur se produit lors de l’appel de méthode ax.plot(x, np.log(x)).
L’erreur se produit parce que nous avons fait par erreur de ax un tableau NumPy, et un tableau NumPy n’a pas de méthode plot.
Mais faisons semblant de ne pas comprendre cela pour le moment.
Nous pourrions soupçonner qu’il y a un problème avec ax, mais lorsque nous essayons d’examiner cet objet, nous obtenons l’exception suivante :
ax
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[3], line 1
----> 1 ax
NameError: name 'ax' is not defined
Le problème est que ax a été défini à l’intérieur de plot_log(), et le nom est
perdu une fois que cette fonction se termine.
Essayons de procéder autrement.
Nous exécutons à nouveau le premier bloc de cellule, générant la même erreur
def plot_log():
fig, ax = plt.subplots(2, 1)
x = np.linspace(1, 2, 10)
ax.plot(x, np.log(x))
plt.show()
plot_log() # Appelle la fonction, génère le graphique
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[4], line 7
3 x = np.linspace(1, 2, 10)
4 ax.plot(x, np.log(x))
5 plt.show()
6
----> 7 plot_log() # Appelle la fonction, génère le graphique
Cell In[4], line 4, in plot_log()
1 def plot_log():
2 fig, ax = plt.subplots(2, 1)
3 x = np.linspace(1, 2, 10)
----> 4 ax.plot(x, np.log(x))
5 plt.show()
AttributeError: 'numpy.ndarray' object has no attribute 'plot'
Mais cette fois, nous tapons le bloc de cellule suivant
%debug
Vous devriez être basculé dans une nouvelle invite qui ressemble à ceci
ipdb>
(Vous verrez peut-être pdb> à la place)
Nous pouvons maintenant examiner la valeur de nos variables à ce point du programme, avancer pas à pas dans le code, etc.
Par exemple, ici nous tapons simplement le nom ax pour voir ce qui se passe avec
cet objet :
ipdb> ax
array([<matplotlib.axes.AxesSubplot object at 0x290f5d0>,
<matplotlib.axes.AxesSubplot object at 0x2930810>], dtype=object)
Il est maintenant très clair que ax est un tableau, ce qui clarifie la source du
problème.
Pour savoir ce que vous pouvez faire d’autre depuis l’intérieur de ipdb (ou pdb), utilisez l’aide
en ligne
ipdb> h
Documented commands (type help <topic>):
========================================
EOF bt cont enable jump pdef r tbreak w
a c continue exit l pdoc restart u whatis
alias cl d h list pinfo return unalias where
args clear debug help n pp run unt
b commands disable ignore next q s until
break condition down j p quit step up
Miscellaneous help topics:
==========================
exec pdb
Undocumented commands:
======================
retval rv
ipdb> h c
c(ont(inue))
Continue execution, only stop when a breakpoint is encountered.
22.2.2. Définir un point d’arrêt#
L’approche précédente est pratique mais parfois insuffisante.
Considérons la version modifiée suivante de notre fonction ci-dessus
def plot_log():
fig, ax = plt.subplots()
x = np.logspace(1, 2, 10)
ax.plot(x, np.log(x))
plt.show()
plot_log()
Ici, le problème d’origine est corrigé, mais nous avons accidentellement écrit
np.logspace(1, 2, 10) au lieu de np.linspace(1, 2, 10).
Maintenant, il n’y aura pas d’exception, mais le graphique ne sera pas correct.
Pour investiguer, il serait utile de pouvoir inspecter des variables comme x pendant l’exécution de la fonction.
À cette fin, nous ajoutons un « point d’arrêt » en insérant breakpoint() à l’intérieur du bloc de code de la fonction
def plot_log():
breakpoint()
fig, ax = plt.subplots()
x = np.logspace(1, 2, 10)
ax.plot(x, np.log(x))
plt.show()
plot_log()
Maintenant, exécutons le script et investiguons via le débogueur
> <ipython-input-6-a188074383b7>(6)plot_log()
-> fig, ax = plt.subplots()
(Pdb) n
> <ipython-input-6-a188074383b7>(7)plot_log()
-> x = np.logspace(1, 2, 10)
(Pdb) n
> <ipython-input-6-a188074383b7>(8)plot_log()
-> ax.plot(x, np.log(x))
(Pdb) x
array([ 10. , 12.91549665, 16.68100537, 21.5443469 ,
27.82559402, 35.93813664, 46.41588834, 59.94842503,
77.42636827, 100. ])
Nous avons utilisé n deux fois pour avancer pas à pas dans le code (une ligne à la fois).
Puis nous avons affiché la valeur de x pour voir ce qui se passait avec cette variable.
Pour quitter le débogueur, utilisez q.
22.2.3. Autres commandes magiques utiles#
Dans ce cours, nous avons utilisé la commande magique IPython %debug.
Il existe de nombreuses autres commandes magiques utiles :
%precision 4définit la précision d’affichage des flottants à 4 décimales%whosdonne une liste des variables et de leurs valeurs%quickrefdonne une liste des commandes magiques
La liste complète des commandes magiques est ici.
22.3. Gestion des erreurs#
Il est parfois possible d’anticiper les bogues et les erreurs au moment où nous écrivons le code.
Par exemple, la variance empirique non biaisée de l’échantillon \(y_1, \ldots, y_n\) est définie comme
Cela peut être calculé dans NumPy en utilisant np.var.
Mais si vous écriviez une fonction pour gérer un tel calcul, vous pourriez anticiper une erreur de division par zéro lorsque la taille de l’échantillon est égale à un.
Une action possible est de ne rien faire — le programme va simplement planter et afficher un message d’erreur.
Mais il vaut parfois la peine d’écrire votre code de manière à anticiper et à gérer les erreurs d’exécution qui pourraient survenir selon vous.
Pourquoi ?
Parce que les informations de débogage fournies par l’interpréteur sont souvent moins utiles que celles qui peuvent être fournies par un message d’erreur bien rédigé.
Parce que les erreurs qui provoquent l’arrêt de l’exécution interrompent les flux de travail.
Parce que cela réduit la confiance de vos utilisateurs dans votre code (si vous écrivez pour d’autres).
Dans cette section, nous discuterons des différents types d’erreurs en Python et des techniques pour gérer les erreurs potentielles dans nos programmes.
22.3.1. Les erreurs en Python#
Nous avons vu AttributeError et NameError dans nos exemples précédents.
En Python, il existe deux types d’erreurs – les erreurs de syntaxe et les exceptions.
Voici un exemple d’un type d’erreur courant
def f:
Cell In[6], line 1
def f:
^
SyntaxError: expected '('
Comme une syntaxe illégale ne peut pas être exécutée, une erreur de syntaxe met fin à l’exécution du programme.
Voici un autre type d’erreur, sans rapport avec la syntaxe
1 / 0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[7], line 1
----> 1 1 / 0
ZeroDivisionError: division by zero
En voici une autre
x1 = y1
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[8], line 1
----> 1 x1 = y1
NameError: name 'y1' is not defined
Et encore une autre
'foo' + 6
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[9], line 1
----> 1 'foo' + 6
TypeError: can only concatenate str (not "int") to str
Et encore une autre
X = []
x = X[0]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[10], line 2
1 X = []
----> 2 x = X[0]
IndexError: list index out of range
À chaque occasion, l’interpréteur nous informe du type d’erreur
NameError,TypeError,IndexError,ZeroDivisionError, etc.
En Python, ces erreurs sont appelées des exceptions.
22.3.2. Assertions#
Parfois, les erreurs peuvent être évitées en vérifiant si votre programme s’exécute comme prévu.
Un moyen relativement simple de gérer les vérifications est le mot-clé assert.
Par exemple, imaginons un instant que la fonction np.var n’existe
pas et que nous devons écrire la nôtre
def var(y):
n = len(y)
assert n > 1, 'Sample size must be greater than one.'
return np.sum((y - y.mean())**2) / float(n-1)
Si nous exécutons ceci avec un tableau de longueur un, le programme se terminera et affichera notre message d’erreur
var([1])
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
Cell In[12], line 1
----> 1 var([1])
Cell In[11], line 3, in var(y)
1 def var(y):
2 n = len(y)
----> 3 assert n > 1, 'Sample size must be greater than one.'
4 return np.sum((y - y.mean())**2) / float(n-1)
AssertionError: Sample size must be greater than one.
L’avantage est que nous pouvons
échouer tôt, dès que nous savons qu’il y aura un problème
fournir des informations spécifiques sur la raison de l’échec d’un programme
22.3.3. Gestion des erreurs pendant l’exécution#
L’approche utilisée ci-dessus est un peu limitée, car elle conduit toujours à la terminaison.
Parfois, nous pouvons gérer les erreurs de manière plus élégante, en traitant les cas particuliers.
Voyons comment cela se fait.
22.3.3.1. Capturer les exceptions#
Nous pouvons capturer et traiter les exceptions à l’aide de blocs try – except.
Voici un exemple simple
def f(x):
try:
return 1.0 / x
except ZeroDivisionError:
print('Error: division by zero. Returned None')
return None
Lorsque nous appelons f, nous obtenons la sortie suivante
f(2)
0.5
f(0)
Error: division by zero. Returned None
f(0.0)
Error: division by zero. Returned None
L’erreur est capturée et l’exécution du programme n’est pas interrompue.
Notez que les autres types d’erreurs ne sont pas capturés.
Si nous craignons que l’utilisateur passe une chaîne de caractères, nous pouvons également capturer cette erreur
def f(x):
try:
return 1.0 / x
except ZeroDivisionError:
print('Error: Division by zero. Returned None')
except TypeError:
print(f'Error: x cannot be of type {type(x)}. Returned None')
return None
Voici ce qui se passe
f(2)
0.5
f(0)
Error: Division by zero. Returned None
f('foo')
Error: x cannot be of type <class 'str'>. Returned None
Si nous nous sentons paresseux, nous pouvons capturer ces erreurs ensemble
def f(x):
try:
return 1.0 / x
except:
print(f'Error. An issue has occurred with x = {x} of type: {type(x)}')
return None
Voici ce qui se passe
f(2)
0.5
f(0)
Error. An issue has occurred with x = 0 of type: <class 'int'>
f('foo')
Error. An issue has occurred with x = foo of type: <class 'str'>
En général, il vaut mieux être précis.
22.4. Exercices#
Exercice 22.1
Supposons que nous ayons un fichier texte numbers.txt contenant les lignes suivantes
prices
3
8
7
21
En utilisant try – except, écrivez un programme qui lit le contenu du fichier et additionne les nombres, en ignorant les lignes sans nombres.
Vous pouvez utiliser la fonction open() que nous avons apprise précédemment pour ouvrir numbers.txt.
Solution
Enregistrons d’abord les données
%%file numbers.txt
prices
3
8
7
21
Writing numbers.txt
f = open('numbers.txt')
total = 0.0
for line in f:
try:
total += float(line)
except ValueError:
pass
f.close()
print(total)
39.0