• 4 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 27/11/2019

Gérez les erreurs et les bogues

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

 

Dans le chapitre précédent nous avons vu comment ajouter des arguments en ligne de commande à un programme et comment les utiliser. Mais que se passe-t-il si un argument nécessaire au bon fonctionnement du programme est absent ? Python affichera une erreur. Dans ce chapitre nous verrons comment gérer les erreurs que nous pouvons anticiper !

 

Deboguer Python avec PDB

Jusqu'à maintenant nous avons écrit beaucoup de print() dans le programme pour vérifier quelles étaient les informations renvoyées. Et si vous pouviez "geler" le programme à un certain endroit, interagir avec lui puis le quitter ? En résumé : ouvrir une console interactive à l'endroit que vous le désirez ! Ce serait plus facile, non ?  😉  La librairie standard de Python intègre justement cette fonctionnalité-là ! Le module s'appelle PDB pour Python Debugger.

Importez Python Debugger:

parite.py

...
import pdb

 

Vous pouvez désormais utiliser toutes les méthodes du module. Entre toutes, j'aimerais surtout vous parler de set_trace() qui vous permet d'entrer dans le debugger pendant que votre programme tourne.

Définissez un point d'arrêt (breakpoint en anglais) à l'endroit où vous souhaitez interrompre le programme :

parite.py

if __name__ == '__main__':
    args = parse_arguments()
    import pdb; pdb.set_trace()
    if args.extension == 'xml':
        x_an.launch_analysis(args.datafile)
    elif args.extension == 'csv':
        c_an.launch_analysis(args.datafile)

 

Au lancement du programme, celui-ci s'arrête pour vous laisser interagir :

$ python parite.py -e csv -d current_mps.csv
> /chemin/vers/votre/projet/parite.py(16)<module>()
-> if args.extension == 'xml':
(Pdb) args
(Pdb) print(args)
Namespace(datafile='current_mps.csv', extension='csv')
(Pdb) print(args.extension)

 

Notez que la console affiche désormais   (Pdb)   à gauche, indiquant que vous interagissez ici avec le débogueur.

Pour quitter PDB, tapez exit().

 

Erreurs et exceptions

Vous avons vu comment déboguer plus efficacement, mais qu'en est-il des messages d'erreur ? Actuellement, si vous indiquez un fichier qui n'existe pas en argument, le programme s'interrompt.

$ python parite.py -e csv -d dontexist                            
Opening data file dontexist from directory 'data'
Traceback (most recent call last):
  File "parite.py", line 19, in <module>
    c_an.launch_analysis(args.datafile)
  File "/path_to_project/analysis/csv.py", line 14, in launch_analysis
    with open(path_to_file,"r") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'data/dontexist'

 

Il s'agit d'une erreur assez facilement prévisible. Vous allez dans cette partie :

  • Afficher un message d'erreur personnalisé si le fichier n'est pas trouvé.

  • Afficher un message d'erreur si aucun fichier n'est passé en argument.

  • Afficher un message final indiquant que l'analyse a bien eu lieu.

 

Le bloc try / except

Lorsque nous savons qu'une erreur peut survenir, il est intéressant d'intégrer une instruction dans un bloc try / except. Que fait ce bloc ? Il essaie d'exécuter l'instruction mais, si une exception est levée, il exécute une autre instruction.

Voici un bloc très basique :

try:
    0 / "la tête à toto"
except:
    print("0 + 0")

 

Il est évidemment impossible de diviser un nombre par une chaîne de caractères. Python rencontrant une erreur, il passe dans le bloc except et exécute la commande print("0 + 0").

Vous pouvez également spécifier le type d'erreur attendue et afficher le message d'erreur :

try:
    0 / "la tête à Toto"
except TypeError as e:
    print("0 + 0 -- Message d'erreur plus sérieux : ", e)

 

Python écrit alors :

0 + 0 -- Message d'erreur plus sérieux :  unsupported operand type(s) for /: 'int' and 'str'

 

Une autre fonctionnalité intéressante est la possibilité d'exécuter une instruction dans tous les cas, quoi qu'il se passe, en utilisant finally :

try:
    0 / "la tête à Toto"
except TypeError as e:
    print("0 + 0 -- Message d'erreur plus sérieux : ", e)
finally:
    print("La tête à Toto")

 

Enfin, vous pouvez séparer la ligne qui risque de lever une erreur des autres instructions en utilisant else. Ce bloc sera déclenché uniquement si l'instruction contenue dans try se vérifie.

 

Récapitulatif

En résumé, voici un bloc try très complet :

try:
    # instruction qui risque de lever une erreur
except Exception as e:
    # Instruction exécutée quand l'exception Exception est lancée
except:
    # Instruction exécutée dans le cas d'une autre erreur
else:
    # Instruction exécutée si l'instruction dans try est vraie
finally:
    # Instruction exécutée dans tous les cas

 

En d'autres termes :

try:
    print('Je suis une instruction problématique')
except Exception as e:
    print('Je suis déclenchée quand Exception est levée')
except:
    print('Une autre erreur est survenue')
else:
    print("Tout s'est bien passé ! Je passe donc à la suite")
finally:
    print("Je suis déterminé : erreur ou pas, je m'affiche dans tous les cas")

 

Application concrète

Voici ce que cela donne dans le projet :

csv.py

try:
    with open(path_to_file,"r") as f:
        preview = f.readline()
        print("Yeah! We managed to read the file. Here is a preview:")
        print(preview)
except FileNotFoundError as e:
    print("Ow :( The file was not found. Here is the original message of the exception :", e)
except:
    print('Destination unknown')

 

Voici ce qui sera imprimé :

$ python parite.py -e csv -d dontexist
Opening data file dontexist from directory 'data'
Ow :( The file was not found. Here is the original message of the exception : [Errno 2] No such file or directory: 'data/dontexist'

 

Comment trouver le type d'exception qui sera levé ? Je ne les connais pas tous par coeur !

Je vous rassure : moi non plus ! En général vous lancez votre programme et vous notez le nom de l'erreur qui s'affiche. Dans l'exemple suivant, le type d'erreur est   FileNotFoundError:

$ python parite.py -e csv -d dontexist                            
Opening data file dontexist from directory 'data'
Traceback (most recent call last):
  File "parite.py", line 19, in <module>
    c_an.launch_analysis(args.datafile)
  File "/path_to_project/analysis/csv.py", line 14, in launch_analysis
    with open(path_to_file,"r") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'data/dontexist'

La documentation de Python intègre également une liste de toutes les exceptions génériques. Lire la documentation.

 

Lever une exception

Il peut arriver que vous souhaitiez lever une exception avant que le problème ne se produise. Notre programme en est un parfait exemple.

Que se passe-t-il si aucun nom de fichier n'est entré ? Une erreur est certes levée mais pas à l'endroit espéré :

$ python parite.py -e csv                             
Traceback (most recent call last):
  File "parite.py", line 19, in <module>
    c_an.launch_analysis(args.datafile)
  File "path_to_project/analysis/csv.py", line 7, in launch_analysis
    path_to_file = os.path.join("data", data_file)
  File "path_to_project/env/bin/../lib/python3.6/posixpath.py", line 92, in join
    genericpath._check_arg_types('join', a, *p)
  File "path_to_project/env/bin/../lib/python3.6/genericpath.py", line 149, in _check_arg_types
    (funcname, s.__class__.__name__)) from None
TypeError: join() argument must be str or bytes, not 'NoneType'

 

Ici, Python indique qu'il utilise la méthode join() avec des arguments de type 'string' ou 'bytes' uniquement, et non 'NoneType'. Cette méthode se trouve dans le fichier csv.py à la ligne 7. Vous pourriez bien sûr englober cette instruction dans un bloc   try   mais, alors, il vous faudrait procéder de la même manière pour le fichier xml.py. Cela dupliquera votre code et ce n'est pas une bonne idée.

Comment faire ?

Prenez le problème à la racine et levez une exception si aucun fichier n'est passé en argument.

parite.py

if __name__ == '__main__':
    args = parse_arguments()
    try:
        datafile = args.datafile
        if datafile == None:
            raise Warning('You must indicate a datafile!')
        else:
            try:
                if args.extension == 'xml':
                    x_an.launch_analysis(datafile)
                elif args.extension == 'csv':
                    c_an.launch_analysis(datafile)
            except FileNotFoundError as e:
                print("Ow :( The file was not found. Here is the original message of the exception :", e)
            finally:
                print('#################### Analysis is over ######################')
    except Warning as e:
        print(e)

Comment choisir l'exception à lever ?

Une excellente pratique qui vous aidera à choisir l'exception à lever est d'écrire des tests unitaires, voire de coder en Test-Driven Development ! Sinon, inspirez-vous de la documentation de Python qui est très bien faite. Regardez également la hiérarchie des exceptions ici.

 

Logs

Cela commence à prendre forme ! Mais le projet contient beaucoup de print() qui ont des objectifs  différents. Parfois le message est crucial car il correspond à une exception qui a été levée. D'autres fois, il s'agit simplement d'un message informatif ('Analysis is over').

Comment instaurer des niveaux différents ? Utilisez la librairie logging qui permet justement d'afficher des messages en fonction de leur niveau d'importance.

 

Votre premier log

Logging fait partie de la librairie standard de Python. Tout comme PDB, vous n'avez pas besoin de l'installer.

Importez-la en haut de votre fichier :

import logging as lg

 

Puis remplacez le print() du bloc except Warning par l'instruction suivante :

lg.warning()

 

Voici désormais ce qui s'affiche :

$ python parite.py -e csv      
WARNING:root:You must indicate a datafile!
#################### Analysis is over ######################

Le niveau du message est indiqué en majuscules sur la gauche, suivi du message.

 

Niveaux de log

Logging offre 5 niveaux de log différents :

  • DEBUG : Informations détaillées dans le but d'en savoir plus sur l'exécution d'une instruction.

  • INFO : Information sur le déroulement d'un programme.

  • WARNING : Quelque chose d'inattendu s'est produit mais le programme continue de fonctionner.

  • ERROR : A cause d'un problème important, le programme n'a pu réaliser une tâche.

  • CRITICAL : Problème très sérieux qui a pu causer l'arrêt du programme.

 

Ces cinq niveaux de log sont indicatifs car les méthodes auront le même impact : elles afficheront un message dans la console. Simplement, le niveau ne sera pas le même. C'est à vous de choisir celui que vous désirez.

Par défaut, logging affiche dans la console les messages dont le niveau est supérieur à WARNING. Les logs dont le niveau est DEBUG ou INFO ne seront pas affichés.

Pour les afficher tout de même, indiquez en haut de votre script le niveau minimal attendu :

lg.basicConfig(level=lg.DEBUG)

 

Vous pouvez même aller plus loin en indiquant le niveau de debug souhaité en paramètre !

def parse_arguments():
    parser = argparse.ArgumentParser()
    ...
    parser.add_argument("-v", "--verbose", action='store_true', help="""Make the application talk!""")
    return parser.parse_args()

if __name__ == '__main__':
    args = parse_arguments()
    if args.verbose:
        lg.basicConfig(level=lg.DEBUG)
    ...

 

Je vous laisse remplacer les différents print() du programme par les niveaux de log qui vous paraissent les plus opportuns. Je vous retrouve dans la partie suivante pour créer notre premier graphique !

 

A vous de jouer !

Cliquez sur ce lien

 

Code du chapitre

Retrouvez le code de ce chapitre en cliquant ici. 

Exemple de certificat de réussite
Exemple de certificat de réussite