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." « 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 :
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
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
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 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"))
|
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) 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) 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) 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) 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) 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) |
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 :
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.
|
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")) 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) |