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'
_images/562a6d11656c4dbf753c2694b957d42ee4c7ddb9af51bbb2c80bcc19da00446d.png

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'
_images/562a6d11656c4dbf753c2694b957d42ee4c7ddb9af51bbb2c80bcc19da00446d.png

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()
_images/547410c7340cae7360964f988dd812ff04a4a246d4f35866bf2d0c5c141b8802.png

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 4 définit la précision d’affichage des flottants à 4 décimales

  • %whos donne une liste des variables et de leurs valeurs

  • %quickref donne 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

\[ s^2 := \frac{1}{n-1} \sum_{i=1}^n (y_i - \bar y)^2 \qquad \bar y = \text{ moyenne empirique} \]

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 tryexcept.

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 tryexcept, é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.