11.3 BOGUES ET DEBOGUAGE

 

 

INTRODUCTION

 

La perfection absolue n’est pas de ce monde. On peut considérer que tout programme contient des imperfections. En informatique, celles-ci ne sont pas appelées erreurs, mais bogues (de l’anglais bug = insecte). Les bogues se manifestent lorsque le programme réagit d’une manière non conforme à ce qui est attendu ou s’il se plante. De ce fait, les compétences de déboguage sont aussi vitales que celles permettant d’écrire du code. Il est bien clair entre nous que les codeurs devraient faire de leur mieux pour éviter d’introduire des bogues dans leur programme. Sachant que chaque ligne de code est susceptible de créer un bogue, il faut user de prudence et programmer de manière défensive. Par exemple, lorsque l’on n’est pas totalement convaincu qu’un bout de code ou un algorithme est correct, il est préférable de tester ces lignes indépendamment du reste du code et de se concentrer sur cette partie du code au lieu de foncer la tête dans le guidon en laissant ce travail pour plus tard. De nos jours, quantité de composants logiciels sont responsables de gérer de grandes sommes d’argent ou des appareils mettant des vies en jeu. En tant que développeur de composant logiciel assumant un rôle aussi crucial, il est nécessaire d’assumer la responsabilité de tester chaque ligne de code pour garantir qu’elle fonctionne correctement. La bricole rapide et le tâtonnement sont totalement exclus dans ce genre de cas.

L’objectif crucial du développement d’algorithmes et de gros systèmes logiciels est de produire des programmes qui sont autant que faire se peut dépourvus de bogues. Il existe de nombreuses approches pour parvenir à ce but. On peut utiliser des raisonnements mathématiques pour tenter de démontrer que le programme possède le comportement désiré. Cela peut se faire sans ordinateur, en utilisant des raisonnements mathématiques similaires à ceux utilisés pour prouver la validité d’un théorème. Cela n’est cependant possible que pour des programmes relativement simples. Une autre possibilité consiste à limiter la syntaxe du langage de programmation pour empêcher le programmeur de faire des choses qui pourraient s’avérer dangereuses. Un des exemples typiques a consisté à supprimer des langages modernes la notion de pointeurs présents dans les langages bas niveau et rendant possible le fait de référencer les faux objets en mémoire ou des emplacements mémoires indéfinis. C’est la raison pour laquelle il existe des langages qui imposent volontairement des restrictions de ce type et d’autres, moins sécurisés, qui laissent une marge de manœuvre considérable au programmeur. Le langage Python appartient plutôt à la famille des langages permissifs et adopte la maxime suivante

"We are all adults and decide for ourselves what we do and what we shouldn't."
or "After all, we are all consenting adults here".

« Nous sommes tous des adultes et décidons par nous-mêmes ce que l’on peut faire et ce qu’il ne fait pas faire. » ou « Après tout, nous sommes tous des adultes consentants ».

Un principe important pour créer des programmes qui sont aussi solides que possibles s’appelle la programmation par contrat (Design by Contract (DbC)). Celle-ci remonte au père du langage Eiffel, Bertrand Meyer, à l’ETH Zürich. Meyer envisage un logiciel comme étant un contrat entre un programmeur A et l’utilisateur B, où B pourrait très bien être un programmeur qui utilise les modules et bibliothèques fournis par le programmeur A. Sous forme de contrat, le programmeur A détermine très précisément sous quelles conditions (préconditions / prémisses) son module va fournir un résultat correct et décrit très précisément ce qui est retourné (les postconditions). En d’autres termes, l’utilisateur B se conforme aux préconditions exigées par A de telle sorte qu’il obtient de A la garantie que le résultat qu’il va obtenir est conforme aux postconditions annoncées. B n’a pas besoin de connaître les détails d’implémentation du module et A peut changer cette implémentation n’importe quand aussi longtemps que les postconditions sont respectées.

 

 

ASSERTIONS

 

Du fait que le programmeur A, le producteur du module, ne fait pas trop confiance à l’utilisateur B, il va incorporer dans son code des tests qui permettent de vérifier que les préconditions  nécessaires au bon fonctionnement sont bien remplies. On les appelle des assertions. Si ces assertions ne sont pas respectées, le programme va généralement quitter en levant une exception et en affichant un message d’erreur. On peut aussi imaginer que l’erreur soit gérée sans causer le crash du programme. Dans ce cas, il faut appliquer le principe suivant de génie logiciel :
Il est préférable de stopper l’exécution du programme avec un message d’erreur descriptif et mettant en évidence les causes possibles de l’erreur plutôt que de le laisser tourner pour aboutir à un résultat erroné.

Un programmeur A développe par exemple une fonction sinc(x) (sinus cardinalis) qui joue un rôle très important en traitement de signal. Voici la définition mathématique :

 

Un programmeur B veut l’utiliser pour réaliser une représentation graphique de la fonction pour x variant entre -20 et 20.

 

from gpanel import *
from math import pi, sin

def sinc(x):
    y = sin(x) / x
    return y

makeGPanel(-24, 24, -1.2, 1.2)
drawGrid(-20, 20, -1.0, 1, "darkgray")
title("Sinus Cardinalis: y = sin(x) / x")

x = -20
dx = 0.1
while x <= 20:
    y = sinc(x)
    if x == -20:
        move(x, y)
    else:
        draw(x, y)
    x += dx
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

Au premier coup d’œil, il n’y a pas de souci et le programme semble tourner correctement … dans ce cas précis … En effet, si l’on fixe l’incrément de x à 1, le programme rencontre une erreur malencontreuse :

Une des manières de résoudre ce problème consiste pour A à exiger la précondition que x ne soit pas nul et mettre en œuvre une assertion qui décrit précisément l’erreur. 

 

from gpanel import *
from math import pi, sin

def sinc(x):
    assert x == 0, "Error in sinc(x). x = 0 not allowed"
    y = sin(x) / x
    return y

makeGPanel(-24, 24, -1.2, 1.2)
drawGrid(-20, 20, -1.0, 1, "darkgray")
title("Sinus Cardinalis: y = sin(x) / x")

x = -20
dx = 1
while x <= 20:
    y = sinc(x)
    if x == -20:
        move(x, y)
    else:
        draw(x, y)
    x += dx
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

Le message d’erreur est maintenant bien meilleur puisqu’il indique exactement où se situe l’erreur rencontrée.

Il serait encore bien mieux que le programmeur A traite spécifiquement le cas x = 0 en retournant la valeur limite de la fonction lorsque x tend vers 0. 

 

from gpanel import *
from math import pi, sin

def sinc(x):
    if x == 0:
        return 1.0
    y = sin(x) / x
    return y

makeGPanel(-24, 24, -1.2, 1.2)
drawGrid(-20, 20, -1.0, 1, "darkgray")
title("Sinus Cardinalis: y = sin(x) / x")

x = -20
dx = 1
while x <= 20:
    y = sinc(x)
    if x == -20:
        move(x, y)
    else:
        draw(x, y)
    x += dx
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

Ce code viole cependant la règle fondamentale qu’il faut toujours se méfier des nombres flottants dans les comparaisons à cause des inévitables erreurs d’arrondis effectuées par le processeur. Il serait encore mieux d’effectuer le test en utilisant une valeur de tolérance epsilon :

def sinc(x):
    epsilon = 1e-100
    if abs(x) < epsilon:
        return 1.0

Cette version de la fonction sinc(x) n’est toujours pas complètement bétonnée car il faudrait encore exiger que x soit bien un nombre réel. Si l’on s’aventurait par exemple à effectuer l’appel avec x = "python", une erreur incongrue surviendrait immanquablement :

from math import sin

def sinc(x):
    y = sin(x) / x
    return y

print(sinc("python"))
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

Ceci montre l’avantage des langages de programmation avec déclaration explicite du type de données des variables, ce qui permet une détection de ce genre de problème comme erreur de syntaxe avant son exécution (lors de la compilation / vérification syntaxique) et, de ce fait, avant qu’elle ne touche l’utilisateur.

 

 

AFFICHER DES INFORMATIONS DE DÉBOGUAGE

 

Les bons programmeurs sont reconnus pour leur capacité à éliminer les erreurs très rapidement [plus... Il existe un mouvement de développement logiciel basé sur la notion de développement dirigé par les tests (Test Driven Development (TDD) en anglais)]. Comme nous pouvons tous apprendre de nos erreurs, considérons le programme suivant qui est censé échanger la valeur de deux variables. Il comporte un bogue puisqu’il affiche 2,2 :

def exchange(x, y):
    y = x
    x = y
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

Une stratégie bien connue pour trouver des bugs consiste à écrire vers la console la valeur de certaines variables :

def exchange(x, y):
    print("exchange() with params", x, y)
    y = x
    x = y
    print("exchange() returning", x, y)
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

La source de l’erreur devient ainsi évidente et on comprend tout de suite comment corriger l’anomalie.

def exchange(x, y):
    print("exchange() with params", x, y)
    temp = y
    y = x
    x = temp
    print("exchange() returning", x, y)
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

Maintenant que l’erreur est réglée, les instructions de déboguage sont superflues. Mais au lieu de les supprimer, on peut se contenter de les mettre en commentaires avec le symbole # (Raccourci clavier Ctrl+Q dans TigerJython), ce qui permet éventuellement de les réutiliser par la suite.

def exchange(x, y):
#    print("exchange() with params", x, y)
    temp = y
    y = x
    x = temp
#    print("exchange() returning", x, y)
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

Il est souvent encore plus adapté d’utiliser un fanion de déboguage permettant d’activer ou de désactiver les informations de déboguage en différents endroits du code :

def exchange(x, y):
    if debug: print("exchange() with params", x, y)
    temp = y
    y = x
    x = temp
    if debug: print("exchange() returning", x, y)
    return x, y

debug = False
a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

En Python, il est possible d’échanger les valeurs de deux variables sans créer une autre variable temporaire. Il suffit pour cela d’utiliser sa capacité de faire de l’empaquetage / dépaquetage (packing / unpacking en anglais) automatiquement sur les tuples :

def exchange(x, y):
    x, y = y, x
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)


 

SE SERVIR DU DÉBOGUEUR

 

Les débogueurs sont des outils très importants pour développer des gros systèmes logiciels. Ils permettent d’exécuter un programme très lentement ou même pas à pas, ce qui permet de suivre son exécution avec précision. Ils permettent ainsi d’adapter la vitesse d’exécution phénoménale de l’ordinateur aux capacités cognitives et d’analyse limitées de l’être humain. L’éditeur de TigerJython intègre un débogueur confortable permettant de faciliter une compréhension précise et détaillée du déroulement de la séquence d’instructions du programme. Nous allons maintenant exécuter le programme précédent en utilisant le débogueur :

  Une fois dans l’éditeur, cliquer sur le bouton du débogueur représenté par la coccinelle.


 
Le bouton portant le « 1 » permet d’exécuter le programme pas à pas en observant l’évolution des différentes variables dans la fenêtre de déboguage. Après 8 clics, on peut voir que les deux variables, x et y, possèdent la valeur 2 juste avant que la fonction exchange ne retourne.

Si l’on exécute le programme une fois le bogue réglé, on peut observer que les valeurs des variables sont effectivement échangées à l’intérieur de la fonction exchange(), ce qui mène finalement au résultat attendu. On peut observer également très clairement la durée de vie limitée des variables locales x et y par opposition aux variables globales a et b.

Il est également possible de placer des points d’arrêt dans le programme pour éviter de devoir traverser des parties non pertinentes du code en mode pas à pas. Pour ce faire, cliquer dans la marge gauche, à gauche du numéro de ligne. Un petit fanion rouge représentant le point d’arrêt apparaît alors à cet endroit. Lors d’une pression sur le bouton « exécuter », le programme sera alors exécuté automatiquement et rapidement jusqu’au point d’arrêt et sera ensuite mis en pause.


Ceci permet d’examiner les variables à ce stade de l’exécution du programme. On peut alors poursuivre l’exécution du programme en mode normal ou en mode pas à pas.

Il est également possible de placer plusieurs points d’arrêt. Pour supprimer un point d’arrêt, il suffit de cliquer dessus.

 

 

GESTION DES ERREURS À L’AIDE DES EXCEPTIONS

 

La gestion des erreurs à l’aide des exceptions est classique dans les langages de programmation modernes. Cela consiste à placer le code susceptible d’engendrer des erreurs dans un bloc try.  Si l’erreur survient, l’exécution du bloc est abandonnée et c’est le bloc except qui alors exécuté. Le bloc except permet donc de réagir convenablement à l’erreur. Au pire, on peut y terminer proprement le programme en appelant sys.exit().

On peut par exemple intercepter l’erreur survenant si le paramètre de la fonction sinc(x) n’est pas numérique et gérer celle-ci de manière appropriée :

from sys import exit
from math import sin

def sinc(x):
    try:
        if x == 0:
            return 1.0
        y = sin(x) / x
    except TypeError:
        print("Error in sinc(x). x =", x, "is not a number")
        exit()
    return y

print(sinc("python"))
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

La postcondition pour la fonction sinc(x) pourrait également être que la fonction retourne None si le paramètre est d’un type incorrect. Il revient alors à l’utilisateur de gérer cette erreur convenablement.

from math import sin

def sinc(x):
    try:
        if x == 0:
            return 1.0
        y = sin(x) / x
    except TypeError:
        return None
    return y

y = sinc("python")
if y == None:
   print("Illegal call")
else:
   print(y)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)