IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Think Julia


précédentsommairesuivant

21. Débogage

Lors du débogage, il est nécessaire de distinguer les différents types d'erreurs afin de les repérer rapidement :

  • les erreurs de syntaxe sont découvertes par le compilateur lors de la traduction à la volée du code source en code compilé. Elles indiquent une formulation erronée au sein de la structure du programme. Il en va ainsi, par exemple, de l'omission du mot-clé end à la fin d'un bloc de fonction qui engendre le message d'erreur quelque peu redondant : ERROR: LoadError: syntax: incomplete: function requires end ;

  • les erreurs d'exécution sont produites par le compilateur si un dysfonctionnement apparaît au cours de l'exécution d'un programme. La plupart des messages d'erreur d'exécution contiennent des informations sur la localisation de l'erreur et les fonctions en cours d'exécution. Par exemple, une récursion infinie provoque une erreur d'exécution telle que ERROR: StackOverflowError :

  • les erreurs sémantiques sont liées à un programme qui s'exécute sans produire de messages d'erreur, mais qui ne fait pas ce à quoi il est destiné. À titre d'exemple, une expression peut ne pas être évaluée dans l'ordre prévu, ce qui donne un résultat incorrect.

La première étape du débogage consiste à déterminer le type d'erreur auquel on est confronté. Bien que les sections suivantes soient organisées par type d'erreur, certaines techniques sont applicables dans plus d'une situation.

21-1. Erreurs de syntaxe

Les erreurs de syntaxe sont généralement faciles à corriger dès qu'on a compris de quoi il s'agit. Malheureusement, d'une manière générale, les messages d'erreur ne sont pas très explicites. Les messages les plus courants sont du type ERROR: LoadError: syntax: incomplete: premature end of input et ERROR: LoadError: syntax: unexpected "=". Aucune des deux formulations n'est véritablement instructive.

En revanche, le message indique l'endroit du programme où le problème s'est produit. En fait, il indique où Julia a remarqué un problème, ce qui n'est pas nécessairement l'endroit exact où se situe l'erreur. Il n'est pas rare que celle-ci se trouve en amont de l'emplacement indiqué par le message d'erreur (souvent sur la ligne précédente).

Si vous construisez un programme de manière incrémentale, vous devriez avoir une bonne idée de l'endroit où se trouve l'erreur. Elle se cache soit dans la dernière ligne, soit dans le dernier bloc ajouté.

Si vous copiez le code depuis un livre ou une page de l'Internet, commencez par comparer très soigneusement votre code à celui contenu dans la source. Vérifiez chaque caractère. En même temps, n'oubliez pas que la source peut elle-même contenir un code erroné. Si bien que si vous voyez quelque chose qui ressemble à une erreur de syntaxe, il peut s'agir de ce dernier cas de figure.

Voici quelques moyens d'éviter les erreurs de syntaxe les plus courantes :

  1. Assurez-vous que vous n'employez pas un mot-clé de Julia pour un nom de variable ;
  2. Vérifiez que le mot-clé end clôture chaque déclaration composée, y compris les blocs for, while, if et les blocs de fonction ;

  3. Assurez-vous que toutes les chaînes de caractères du code sont entre guillemets et que tous les guillemets sont des "droits" et non “bouclés” ;

  4. Si vous avez écrit des chaînes multilignes avec des guillemets triples, assurez-vous que vous avez clôturé la chaîne correctement. Une chaîne inachevée peut provoquer une erreur « invalid token » à la fin d'un programme. En outre, elle peut traiter la partie suivante du programme comme une chaîne jusqu'à ce qu'elle parvienne à la chaîne suivante. Dans ce dernier cas, il se peut que la compilation ne produise aucun message d'erreur ;

  5. Un opérateur d'ouverture sans son vis-à-vis — (, { ou [ — amène Julia à considérer la ligne suivante comme appartenant à la déclaration courante. En général, une erreur se produit presque immédiatement à la ligne suivante ;
  6. Vérifiez le classique = au lieu de == à l'intérieur d'un test conditionnel ;

  7. Si des caractères non ASCII se trouvent dans le code (y compris des chaînes de caractères et des commentaires), cela peut poser un problème bien que Julia gère généralement les caractères non ASCII. Soyez attentif si vous collez du texte provenant d'une page web ou d'une autre source.

Si rien de ceci ne fonctionne, passez à la section suivante.

21-1-1. Je continue à faire des changements, mais sans effet

Si le REPL indique qu'il y a une erreur et que vous ne la détectez pas, c'est peut-être parce que vous ne considérez pas le même code que le REPL. Vérifiez votre environnement de programmation afin de déterminer si le programme que vous éditez est bien celui que Julia tente d'exécuter.

Si vous n'en êtes pas sûr, essayez d'introduire une erreur de syntaxe évidente et délibérée au début du programme. Exécutez-le à nouveau. Dans le cas où le REPL ne trouve pas la nouvelle erreur, vous pouvez conclure que vous n'exécutez pas le nouveau code.

Il y a quelques coupables potentiels :

  • vous avez édité le fichier et oublié de sauvegarder les modifications avant de l'exécuter à nouveau. Certains environnements de programmation le font pour vous, d'autres non ;
  • vous avez changé le nom du fichier, cependant vous utilisez toujours l'ancien nom ;
  • quelque chose dans votre environnement de développement est mal configuré ;
  • si vous écrivez un module et que vous l'utilisez, assurez-vous de ne pas donner à votre module le même nom qu'un des modules standard de Julia ;
  • si vous employez le mot-clé using pour importer un module, n'oubliez pas qu'il est impératif de redémarrer le REPL lorsque vous modifiez le code du module. Si vous importez à nouveau le module, il ne se passe rien.

Si vous êtes bloqué et que vous ne pouvez pas comprendre ce qui se produit, une approche consiste à recommencer avec un nouveau programme comme « Hello, World! » et à vous assurer que vous pouvez faire fonctionner un programme opérationnel connu. Ensuite, ajoutez progressivement les éléments du programme d'origine au nouveau programme.

21-2. Erreurs d'exécution

Dès que votre programme est syntaxiquement correct, Julia peut le lire et a minima commencer à l'exécuter. Qu'est-ce qui alors pourrait dysfonctionner ?

21-2-1. Mon programme ne fait absolument rien

Ce problème est assez fréquent lorsque votre fichier se compose de fonctions et de classes sans invoquer réellement une fonction pour en lancer l'exécution. Cela peut être intentionnel si vous prévoyez d'importer ce module uniquement pour fournir des classes et des fonctions.

Si ce n'est pas le cas, assurez-vous qu'un appel de fonction est bien présent dans le programme. Ensuite, vérifiez que le flux d'exécution l'atteint bel et bien (voir le paragraphe 21.2.2.3Flux d'exécution).

21-2-2. Mon programme est « suspendu »

Si un programme s'arrête et semble ne rien faire, il est « suspendu ». Souvent, cela signifie qu'il est engagé dans une boucle ou une récursion infinie :

  • si vous soupçonnez qu'une boucle particulière est à l'origine du problème, ajoutez un message d'affichage immédiatement avant la boucle qui stipule « entrer dans la boucle » et un autre immédiatement après tel que « sortir de la boucle ».
    Exécutez à nouveau le programme. Si vous obtenez le premier message et non le second, vous êtes confronté à une boucle infinie. Passez au paragraphe 21.2.2.1Boucle infinie ci-dessous,
    la plupart du temps, une récursion infinie entraînera l'exécution du programme pendant un certain temps, puis produira une ERROR: LoadError: Error StackOverflowError. Si c'est le cas, passez au paragraphe 21.2.2.2Récursion infinie ci-dessous.
    Si vous n'obtenez pas cette erreur quoique vous soupçonniez un problème avec une méthode ou une fonction récursive, vous pouvez toujours utiliser les techniques du paragraphe 21.2.2.2Récursion infinie ;
  • si aucune de ces étapes n'apporte de solution, commencez à tester d'autres boucles ainsi que d'autres fonctions et méthodes récursives,
    en cas d'échec, il est possible que vous ne compreniez pas le flux d'exécution de votre programme. Rendez-vous au paragraphe 21.2.2.3Flux d'exécution.
21-2-2-1. Boucle infinie

Si vous pressentez une boucle infinie et que vous pensez avoir identifié la boucle responsable du problème, ajoutez une déclaration d'affichage à la fin de la boucle, qui communique les valeurs des variables de la condition et la valeur de la condition.

Par exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
while x > 0 && y < 0 
    # modifier x 
    # modifier y 
    @debug "variables" x y 
    @debug "condition" x > 0 && y < 0 
end

Maintenant, lorsque vous exécutez le programme en mode de débogage, vous verrez la valeur des variables et la condition pour chaque passage dans la boucle. Au dernier passage, la condition devrait être fausse. Si la boucle continue, vous pourrez visualiser les valeurs de x ainsi que d'y, et vous comprendrez peut-être pourquoi elles ne sont pas mises à jour correctement.

21-2-2-2. Récursion infinie

La plupart du temps, une récursion infinie amène un programme à calculer pendant un certain temps pour produire une ERROR: LoadError: Error StackOverflowError.

Si vous pensez qu'une fonction provoque une récursion infinie, contrôlez la présence d'un cas de base. Il devrait y avoir une condition amenant la fonction à effectuer un retour sans exécuter une invocation récursive. Si ce n'est pas le cas, vous devez repenser l'algorithme et identifier un cas de base.

Si un cas de base existe, mais que le programme ne semble pas l'atteindre, ajoutez une instruction d'affichage au début de la fonction, qui indique les paramètres. À présent, lorsque vous exécutez le programme, vous verrez quelques lignes de sortie chaque fois que la fonction est invoquée et vous pourrez analyser les valeurs des paramètres. Si les paramètres ne se déplacent pas vers le cas de base, vous aurez une idée des raisons pour lesquelles il en va ainsi.

21-2-2-3. Flux d'exécution

Si vous n'êtes pas sûr de la manière dont le flux d'exécution percole dans votre programme, ajoutez un message au début de chaque fonction comme « entrer dans la fonction unetelle », où unetelle est le nom de la fonction.

À ce stade, lorsque vous exécutez le programme, celui-ci affichera une trace de chaque fonction au fur et à mesure qu'elle sera invoquée.

21-2-3. Quand j'exécute mon programme, j'obtiens une exception

Si quelque chose capote pendant l'exécution, Julia affiche un message qui comprend le nom de l'exception, la ligne du programme où le problème a surgi et une trace de pile (stacktrace).

La trace de pile identifie la fonction en cours d'exécution, puis la fonction qui l'a appelée, celle qui à son tour a appelé cette dernière, et ainsi de suite. En d'autres termes, elle retrace la séquence des appels de fonction qui ont conduit à la situation à laquelle vous êtes confronté, y compris le numéro de ligne de votre fichier où chaque appel s'est produit.

La première étape consiste à examiner l'endroit du programme où l'erreur s'est produite et à déterminer si vous pouvez comprendre ce qui s'est passé. Il s'agit là de certaines des erreurs d'exécution les plus courantes :

ArgumentError un des arguments à un appel de fonction n'est pas dans l'état attendu ;

BoundsError une opération sur les indices dans un tableau a tenté d'accéder à un élément hors limites ;

DomainError l'argument d'une fonction ou d'un constructeur est en dehors du domaine de validité ;

DivideError une division entière a été tentée avec une valeur de dénominateur égale à 0 ;

EOFError il n'y avait plus de données disponibles à lire à partir d'un fichier ou d'un flux ;

InexactError ne peut pas exactement se convertir à un type ;

KeyError une opération d'indexation dans un objet AbstractDict (Dict) ou Set a tenté d'accéder ou de supprimer un élément inexistant ;

MethodError une méthode avec la signature requise n'existe pas dans la fonction générique donnée. Alternativement, il n'existe pas de méthode unique plus spécifique ;

OutOfMemoryError une opération a alloué trop de mémoire pour que le système ou le récupérateur de mémoire puisse la gérer correctement ;

OverflowError le résultat d'une expression est trop grand pour le type spécifié et provoque un débordement ;

StackOverflowError un appel de fonction tente d'utiliser plus d'espace que celui disponible dans la pile d'appels. Cela se produit généralement lorsqu'un appel est répété à l'infini ;

StringIndexError une erreur s'est produite lors de la tentative d'accès à un indice de chaîne invalide ;

SystemError un appel système a échoué avec un code d'erreur ;

TypeError erreur d'assertion de type, ou erreur produite lors de l'appel d'une fonction intégrée avec un type d'argument incorrect ;

UndefVarError un symbole dans l'environnement en cours n'est pas défini.

21-2-4. J'ai ajouté beaucoup de déclarations d'affichage ; je suis inondé de sorties

L'utilisation intensive des instructions d'affichage pour le débogage se traduit régulièrement en une avalanche de messages. Deux façons d'y remédier : simplifier la sortie ou simplifier le programme.

Pour simplifier la sortie, il est pertinent de supprimer ou commenter les déclarations d'affichage qui n'apportent pas d'aide concrète, les combiner (le cas échéant) ou encore formater la sortie de manière à être plus facile à interpréter.

Pour simplifier un programme, il existe plusieurs approches. Tout d'abord, réduisez le problème sur lequel le programme travaille. Par exemple, si vous traitez une liste, testez une liste courte. Si le programme capte diverses données de l'utilisateur, donnez-lui l'information la plus élémentaire qui cause le problème.

Ensuite, nettoyez le programme. Supprimez le code mort et réorganisez le programme pour le rendre aussi lisible que possible. Imaginons que le problème se situe dans une partie profondément imbriquée du programme. Essayez de réécrire cette partie avec une structure allégée. Si vous présumez l'implication d'une fonction de grande taille, essayez de la diviser en fonctions de moindre taille afin de les tester séparément.

Souvent, le processus de recherche d'un test minimal vous mène au bogue. Si vous trouvez qu'un programme fonctionne dans une situation, mais pas dans une autre, cela aussi vous donne un indice.

De même, réécrire un morceau de code peut aider à débusquer des bogues malicieux. Si vous apportez une modification qui, selon vous, ne devrait pas affecter le programme alors qu'il en est ainsi, vous tenez peut-être bien un indice à défaut de tenir la solution.

21-3. Erreurs sémantiques

Les erreurs sémantiques sont les plus difficiles à déboguer par le simple fait que le compilateur à la volée ne fournit aucune information. Vous seul savez ce que le programme est censé faire.

La première étape consiste à établir un lien entre le code du programme et le comportement observé. Il faut émettre une hypothèse concernant la manière dont le programme procède. Une des causes qui rendent le dépistage des erreurs sémantiques si difficile provient de la vitesse d'exécution des ordinateurs.

On voudrait souvent pouvoir ralentir le programme jusqu'à « vitesse humaine ». L'insertion judicieuse de quelques instructions d'affichage est souvent plus fructueuse et plus rapide que la mise en place d'un débogueur, l'insertion et la suppression de points d'arrêt et la gestion du « pas » du programme pour converger vers l'endroit où l'erreur se produit.

21-3-1. Mon programme ne fonctionne pas

Le moment est venu de se poser quelques questions :

  • le programme était-il censé faire quelque chose qui semble ne pas se produire ? Trouvez la section du code qui remplit cette fonction et vérifiez qu'il s'exécute au moment où vous pensez qu'il devrait procéder ainsi ;
  • quelque chose survient-il qui ne devrait pas se produire ? Trouvez dans votre programme le code qui remplit cette fonction et assurez-vous qu'il s'exécute au moment où il ne devrait pas ;
  • une partie du code produit-elle un effet qui n'est pas celui auquel vous vous attendiez ? Assurez-vous de bien comprendre le code en question, surtout s'il s'agit de fonctions ou de méthodes associées à d'autres modules de Julia. Lisez la documentation relative aux fonctions que vous appelez. Essayez-les en écrivant des cas de test simples et en vérifiant les résultats.

Pour programmer, vous devez disposer d'un modèle mental du fonctionnement des programmes. Si vous écrivez un programme qui n'exécute pas ce que vous attendez, souvent, le problème ne se situe pas dans le programme, mais dans votre modèle mental.

La meilleure façon de corriger votre modèle mental est de fractionner le programme en ses composantes (généralement les fonctions et les méthodes) et de tester chacune d'elles indépendamment. Une fois que vous avez trouvé la distorsion entre votre modèle et la réalité, vous êtes en mesure de résoudre le problème.

Bien entendu, vous devez construire et tester les composants au fur et à mesure que vous développez le programme. Si vous rencontrez un problème, il ne devrait y avoir qu'une petite quantité de nouveau code susceptible d'être incorrect.

21-3-2. J'ai une expression embroussaillée qui ne fait pas ce que j'attends

L'écriture d'expressions complexes reste une bonne pratique pour autant qu'elles restent lisibles. Néanmoins, elles sont souvent difficiles à déboguer. En général, il est judicieux de décomposer une expression complexe en une série d'instructions comportant des variables temporaires.

Par exemple, l'instruction :

 
Sélectionnez
1.
addcard(game.hands[i], popcard(game.hands[findneighbor(game, i)]))

peut être reformulée en :

 
Sélectionnez
1.
2.
3.
neighbor = findneighbor(game, i)
pickedcard = popcard(game.hands[neighbor])
addcard(game.hands[i], pickedcard)

La version explicite est nettement plus facile à lire parce que les noms des variables fournissent une documentation en eux-mêmes. Elle est aussi plus facile à déboguer, car il est possible de vérifier les types des variables intermédiaires et d'afficher leur valeur.

Un autre problème susceptible de survenir avec les expressions de grande taille vient de l'ordre d'évaluation qui n'est peut-être pas celui auquel on s'attend a priori. Par exemple, l'expression kitxmlcodeinlinelatexdvp\frac{x}{2\pi}finkitxmlcodeinlinelatexdvp pourrait être écrite en Julia comme ceci :

 
Sélectionnez
1.
y = x / 2 * π

Toutefois, cette formulation est erronée, car la multiplication et la division ayant la même priorité sont évaluées de gauche à droite. Cette expression calcule donc kitxmlcodeinlinelatexdvp\frac{x}{2}\pifinkitxmlcodeinlinelatexdvp.

Une bonne manière de déboguer les expressions consiste à ajouter des parenthèses pour rendre l'ordre d'évaluation explicite :

 
Sélectionnez
1.
y = x / (2 * π)

Lorsque vous n'êtes pas sûr de l'ordre d'évaluation, utilisez des parenthèses. Non seulement le programme sera correct (dans le sens où il s'exécutera selon votre intention), mais, de surcroît, il sera lisible pour ceux qui n'ont pas mémorisé l'ordre des opérations.

21-3-3. J'ai une fonction qui ne retourne pas ce que j'attends

Si vous écrivez une déclaration de retour avec une expression complexe, vous vous privez de la possibilité d'afficher la valeur de retour avant que l'instruction return soit exécutée. Là encore, il est astucieux de tirer parti d'une variable temporaire. Par exemple, au lieu de :

 
Sélectionnez
1.
return removematches(game.hands[i])

il est approprié d'écrire :

 
Sélectionnez
1.
2.
count = removematches(game.hands[i])
return count

Ainsi, devient-il possible d'afficher la valeur de count avant que l'instruction return ne soit exécutée.

21-3-4. Je suis vraiment, vraiment coincé et j'ai besoin d'aide

D'abord, essayez de vous éloigner de l'ordinateur pendant quelques minutes. Travailler avec un ordinateur peut provoquer ces symptômes :

  • frustration et rage ;
  • croyances superstitieuses (« l'ordinateur me déteste ») et pensées magiques (« le programme ne fonctionne que lorsque je porte ma casquette à l'envers ») ;
  • tenter la programmation aléatoire (c'est-à-dire programmer en écrivant tous les programmes possibles et en choisissant celui qui procède de manière adéquate).

Si vous souffrez de l'un de ces symptômes, levez-vous et allez faire une promenade. Lorsque vous reviendrez au calme, pensez au programme. Que fait-il ? Quelles sont les causes possibles de son comportement ? Quand avez-vous vu le programme fonctionner correctement la dernière fois, qu'avez-vous fait ensuite ?

Parfois, il faut juste du temps pour trouver un bogue. Les très bons programmeurs trouvent régulièrement des bogues quand ils se trouvent loin de leur(s) ordinateur(s) et qu'ils laissent leur esprit vagabonder. Les meilleurs endroits pour trouver des bogues sont les trains, les douches et le lit, juste avant de s'endormir.

21-3-5. Non, j'ai vraiment besoin d'aide

Cela arrive. Même les meilleurs programmeurs se retrouvent occasionnellement bloqués. Parfois, vous travaillez sur un programme depuis si longtemps que vous ne pouvez plus voir l'erreur. Vous avez besoin d'un regard neuf.

Avant de faire appel à quelqu'un d'autre, assurez-vous que vous êtes prêt. Votre programme doit être aussi simple que possible et vous devez travailler sur la plus petite zone possible qui déclenche l'erreur. Vous devez avoir sous la main les instructions d'affichage aux endroits appropriés (et le résultat qu'elles produisent doit être compréhensible). Vous devez comprendre le problème suffisamment bien pour le décrire de manière concise et précise.

Lorsque vous appelez à l'aide, veillez donner les informations nécessaires :

  • s'il y a un message d'erreur, de quoi s'agit-il et quelle partie du programme indique-t-il ?
  • quelle est la dernière modification insérée avant que cette erreur se produise ?
  • quelles sont les dernières lignes de code introduites, quelle est la nouvelle batterie de tests qui échoue ?
  • qu'avez-vous essayé jusque-là et qu'en avez-vous tiré ?

Lorsque vous trouvez le bogue, prenez un peu de temps pour réfléchir à ce que vous auriez pu faire pour le trouver plus rapidement. La prochaine fois que vous verrez quelque chose de similaire, vous pourrez trouver le bogue plus rapidement.

N'oubliez pas que le but n'est pas seulement de faire fonctionner le programme. L'objectif fondamental est d'apprendre à faire fonctionner le programme.


précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Thierry Lepoint et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2021 Developpez.com.