.. _prog-e3-gestion-erreurs: ******************* Gestion des erreurs ******************* Lorsqu'on exécute un programme, il peut arriver que des erreurs se produisent. Ces erreurs peuvent être dues à des problèmes de syntaxe, de logique ou d'exécution. Il est important de savoir comment gérer ces erreurs pour éviter que le programme ne plante et pour fournir des messages d'erreur clairs à l'utilisateur. Les erreurs de syntaxe sont détectées par l'interpréteur ou le compilateur avant l'exécution du programme. Elles se produisent lorsque le code ne respecte pas les règles de la langue de programmation utilisée. Par exemple, une erreur de syntaxe peut se produire si une parenthèse est oubliée ou si un mot-clé est mal orthographié. Les erreurs de logique se produisent lorsque le programme s'exécute sans erreur de syntaxe, mais ne produit pas le résultat attendu. Ces erreurs sont souvent plus difficiles à détecter car elles ne génèrent pas de messages d'erreur explicites. Elles peuvent être causées par des erreurs dans les algorithmes, des conditions incorrectes ou des variables mal initialisées. Les erreurs d'exécution se produisent lorsque le programme est en cours d'exécution et rencontre une situation inattendue, comme la division par zéro ou l'accès à un fichier inexistant. Ces erreurs peuvent provoquer l'arrêt du programme et doivent être gérées de manière appropriée pour éviter des plantages. Nous allons nous concentrer ici sur la gestion des erreurs à l'exécution. .. quiz:: quiz-intro-erreurs :title: Types d'erreurs - :quiz:`{"type":"TF","answer":"F"}` Une exception interrompt systématiquement l'exécution d'un programme - :quiz:`{"type":"TF","answer":"T"}` Une exception non gérée interrompt systématiquement l'exécution d'un programme - Une erreur de :quiz:`{"type":"FB","answer":"syntaxe"}` est détectée avant l'exécution du programme. - :quiz:`{"type":"TF","answer":"T"}` Les erreurs de logique génèrent des messages d'erreur peu explicites. - Les erreurs :quiz:`{"type":"FB","answer":"d'exécution"}` peuvent arrêter un programme. - Les erreurs de syntaxe sont détectées :quiz:`{"type":"FB","answer":"avant l'exécution"}` du programme. - :quiz:`{"type":"TF","answer":"T"}` On peut intercepter un problème d'exécution avant qu'il n'interrompe le programme - :quiz:`{"type":"TF","answer":"F"}` Il est obligatoire d'intercepter un problème d'exécution avant qu'il n'interrompe le programme - :quiz:`{"type":"TF","answer":"T"}` Il est souhaitable d'intercepter un problème d'exécution avant qu'il n'interrompe le programme Langage C ========= En langage C, les erreurs a l'exécution du programme sont souvent gérées en utilisant des codes de retour et des messages d'erreur. Les fonctions peuvent retourner des valeurs spéciales pour indiquer qu'une erreur s'est produite, et il est courant d'utiliser la fonction ``perror()`` pour afficher un message d'erreur correspondant à l'erreur rencontrée. Le langage C ne possède **aucun mécanisme d'exception** permettant d'intercepter les erreurs comparable à ceux de langages de plus haut niveau comme Python ou Java. La gestion des erreurs repose sur plusieurs mécanismes, qui peuvent être combinés. .. quiz:: quiz-c-intro :title: Gestion d'erreurs en C - :quiz:`{"type":"TF","answer":"F"}` Le langage C possède un mécanisme d'exception. - :quiz:`{"type":"TF","answer":"F"}` La fonction ``perror()`` arrête le programme. - La gestion des erreurs en C repose sur des :quiz:`{"type":"FB","answer":"codes de retour"}` et des messages d'erreur. Les codes de retour ------------------- C'est la méthode la plus courante pour signaler une erreur. Une fonction retourne un code indiquant si l'opération a réussi ou échoué. Le code de retour est généralement un entier, où une valeur de zéro indique le succès et une valeur non nulle indique une erreur. .. code-block:: c int lire_fichier(const char *nom) { FILE *f = fopen(nom, "r"); if (f == NULL) return -1; /* ... */ fclose(f); return 0; } int main(void) { if (lire_fichier("test.txt") != 0) { printf("Erreur\n"); } } C'est l'approche privilégiée dans la bibliothèque standard. .. quiz:: quiz-c-retour :title: Codes de retour en C - Pour indiquer le succès, une fonction retourne généralement :quiz:`{"type":"FB","answer":"0","size":1}` - Une valeur :quiz:`{"type":"FB","answer":"non nulle","flags":"fuzzy"}` indique une erreur. - :quiz:`{"type":"TF","answer":"T"}` Vérifier le code de retour d'une fonction est une bonne pratique. - :quiz:`{"type":"TF","answer":"T"}` Une fonction qui échoue peut retourner -1 comme code d'erreur. La variable globale ``errno`` ----------------------------- Certaines fonctions de la bibliothèque standard renseignent la variable globale `errno`. .. code-block:: c FILE *f = fopen("test.txt", "r"); if (f == NULL) { perror("fopen"); } Ici, l'erreur est séparée du code de retour. .. quiz:: quiz-c-errno :title: Variable errno en C - La variable ``errno`` est renseignée par :quiz:`{"type":"SC","values":"toutes les,certaines","answer":"certaines"}` fonctions de la bibliothèque standard - :quiz:`{"type":"TF","answer":"T"}` ``errno`` est une variable globale. - La fonction ``perror()`` affiche un message d'erreur correspondant à :quiz:`{"type":"FB","answer":"errno"}` Les paramètres de sortie ------------------------ En C une fonction ne peut pas retourner plusieurs valeurs. Certaines fonctions retournent donc le résultat via un pointeur et utilisent la valeur de retour pour signaler une erreur. .. code-block:: c int division(int a, int b, int *resultat) { if (b == 0) return -1; *resultat = a / b; return 0; } int main(void) { int r; if (division(10, 2, &r) == 0) printf("%d\n", r); else printf("Division impossible\n"); } .. quiz:: quiz-c-params :title: Paramètres de sortie en C - En C, pour retourner plusieurs valeurs, on peut utiliser des :quiz:`{"type":"FB","answer":"pointeurs"}` - :quiz:`{"type":"TF","answer":"T"}` Un paramètre de sortie permet de retourner le résultat d'une fonction. - Dans l'exemple, si ``b`` vaut 0, la fonction retourne :quiz:`{"type":"FB","answer":"-1","size":2}` - En cas d'erreur la fonction division retourne :quiz:`{"type":"SC","values":"le résultat,un code d'erreur,la valeur de b","answer":"un code d'erreur"}` Les fonctions `abort()` et `assert()` ------------------------------------- La fonction ``abort()`` arrête immédiatement le programme en cours d'exécution. Elle est souvent utilisée pour signaler des erreurs graves ou irrécupérables. On utilise la fonction ``assert()`` pour vérifier une condition. Si cette condition n'est pas respectée, un affichage est produit dans le terminal, et ``abort()`` est déclenchée pour arrêter le programme. Cela est souvent utilisé pour vérifier les préconditions d'une fonction. .. code-block:: c #include #include void f(int *ptr) { assert(ptr != NULL); /* Vérifie que ptr n'est pas NULL */ /* ... */ } int main(void) { f(NULL); /* Cela déclenchera l'assertion et arrêtera le programme */ return 0; } Le programme ci dessus s'arrête immédiatement. .. quiz:: quiz-c-assert :title: assert() et abort() en C - La fonction ``abort()`` :quiz:`{"type":"SC","values":"arrête immédiatement,continue, mets en pause","answer":"arrête immédiatement"}` le programme - :quiz:`{"type":"TF","answer":"T"}` ``assert()`` vérifie une condition. - :quiz:`{"type":"TF","answer":"T"}` Si une assertion échoue, ``abort()`` est automatiquement appelée. - Les assertions sont souvent utilisées pour vérifier les :quiz:`{"type":"FB","answer":"préconditions","flags":"fuzzy"}` d'une fonction. - Les assertions sont vérifiées :quiz:`{"type":"SC","values":"lors de la compilation,lors de l'exécution,à aucun moment","answer":"lors de l'exécution"}` .. _python-exceptions: Python ====== Python dispose d'un mécanisme d'exception intégré qui permet de gérer les erreurs de manière plus structurée et lisible. Les exceptions sont des objets qui représentent des erreurs ou des situations exceptionnelles qui se produisent pendant l'exécution d'un programme. Une exception est une erreur qui se produit lors de l'exécution du programme. Gérer une exception c'est interrompre le cours normal du programme pour déclencher un traitement particulier permettant de poursuivre, ou pas, son exécution. .. quiz:: quiz-python-intro :title: Introduction aux exceptions en Python - :quiz:`{"type":"TF","answer":"F"}` En Python, une exception confirme la règle - Les exceptions en Python sont des :quiz:`{"type":"FB","answer":"objets"}` - :quiz:`{"type":"TF","answer":"T"}` Une exception interrompt le cours normal du programme. - Gérer une exception permet de :quiz:`{"type":"FB","answer":"poursuivre","flags":"fuzzy"}` l'exécution du programme après capture de l'erreur. - Une exception est déclenchée :quiz:`{"type":"SC","values":"avant l'exécution,pendant l'exécution,après l'exécution","answer":"pendant l'exécution"}` du programme. Une vidéo de présentation des exceptions... .. raw:: html Quelques exceptions courantes ----------------------------- Il existe un très grand nombre `d'exceptions `_ organisées de façon structurée et hiérarchique. Voici les plus courantes. Les erreurs de syntaxe ...................... Python n'étant pas un langage compilé, les erreurs dans la rédaction du code ne sont détectées que pendant la phase d'exécution et déclenchent des exceptions. Ainsi les erreurs de syntaxe sont elles même des exceptions de type :py:exc:`SyntaxError`. .. code-block:: python >>> if x%2 == 0 print('x est pair') File "", line 1 if x%2 == 0 print('x est pair') ^ SyntaxError: invalid syntax Une exception de type :py:exc:`IndentationError` se produit lorsque l'on indente mal son programme. .. code-block:: python >>> if True: ... print('ligne indentée avec une tabulation') ... print('ligne indentée avec 4 espaces') File "", line 3 print('4 espaces') ^ IndentationError: unindent does not match any outer indentation level Pour éviter ce type d'erreur, on peut paramétrer son éditeur de texte : - en visualisant les caractères non imprimables ; - en remplaçant le caractère ``tab`` par 4 espaces. .. quiz:: quiz-python-syntaxe :title: Erreurs de syntaxe en Python - Une erreur de syntaxe déclenche une exception de type :quiz:`{"type":"FB","answer":"SyntaxError"}` - :quiz:`{"type":"TF","answer":"T"}` Python détecte les erreurs de syntaxe pendant l'exécution. - Une ``IndentationError`` est déclenchée quand on :quiz:`{"type":"FB","answer":"indente mal","flags":"fuzzy"}` un programme. - Pour éviter les erreurs d'indentation, on peut remplacer le caractère :quiz:`{"type":"FB","answer":"tab"}` par :quiz:`{"type":"FB","answer":"4 espaces"}` Les erreurs de conception ......................... Même si la syntaxe est correcte, d'autres problèmes de conception peuvent survenir. L'appel à une référence qui n'existe pas est une exception de type :py:exc:`NameError`. .. code-block:: python >>> if x%2 == 0: print('x est pair') ... Traceback (most recent call last): File "", line 1, in NameError: name 'x' is not defined Il peut également se produire un problème lors de l'importation d'un module. .. code-block:: python >>> import un_module_qui_nexiste_pas Traceback (most recent call last): File "", line 1, in ImportError: No module named 'un_module_qui_nexiste_pas' ou lorsqu'on applique une fonction a un objet inapproprié. .. code-block:: python >>> len(True) Traceback (most recent call last): File "", line 1, in TypeError: object of type 'bool' has no len() .. quiz:: quiz-python-conception :title: Erreurs de conception en Python - Une exception ``NameError`` est déclenchée quand on utilise une variable :quiz:`{"type":"FB","answer":"non définie","flags":"fuzzy"}` - :quiz:`{"type":"TF","answer":"F"}` Une ``NameError`` est une erreur de syntaxe. - Une ``ImportError`` est déclenchée lors de l'échec de l'import d'un :quiz:`{"type":"FB","answer":"module","flags":"fuzzy"}` - :quiz:`{"type":"TF","answer":"T"}` Une ``TypeError`` est déclenchée quand on utilise un objet inapproprié. - :quiz:`{"type":"SC","values":"NameError,TypeError,ImportError","answer":"TypeError"}` Quelle exception est déclenchée par ``len(True)`` ? Les exceptions numériques ......................... Au cours de l'exécution d'un programme syntaxiquement correct et bien conçu (en ce sens qu'il ne déclenche pas les exceptions précédentes), il peut arriver qu'une opération conduise à une erreur. C'est par exemple le cas lorsqu'un calcul numérique potentiellement "dangereux" utilise un résultat non connu à l'avance. .. code-block:: python >>> res = 0 >>> 1/res Traceback (most recent call last): File "", line 1, in ZeroDivisionError: division by zero ou dépassant les capacités de la machine... .. code-block:: python >>> 2.0**4000 Traceback (most recent call last): File "", line 1, in OverflowError: (34, 'Result too large') .. quiz:: quiz-python-numerique :title: Exceptions numériques en Python - Une ``ZeroDivisionError`` est déclenchée lors d'une :quiz:`{"type":"FB","answer":"division par zéro","flags":"fuzzy"}` - :quiz:`{"type":"TF","answer":"T"}` Une ``OverflowError`` est déclenchée quand un calcul dépasse les capacités de la machine. - ``1/0`` déclenche une exception de type :quiz:`{"type":"SC","values":"ZeroDivisionError,OverflowError,TypeError","answer":"ZeroDivisionError"}` - :quiz:`{"type":"TF","answer":"F"}` Les exceptions numériques peuvent être évitées par des tests préalables dans tous les cas. L'accès à des ressources externes ................................. Lorsque le programme tente d'accéder à une ressource qui n'existe pas ou qui n'est pas disponible au moment de l'exécution, une exception de type :py:exc:`FileNotFoundError` est déclenchée. .. code-block:: python >>> open('un_fichier_qui_nexiste_pas.txt', 'r') Traceback (most recent call last): File "", line 1, in FileNotFoundError: [Errno 2] No such file or directory: 'un_fichier_qui_nexiste_pas.txt' .. quiz:: quiz-python-ressources :title: Exceptions d'accès aux ressources en Python - Une ``FileNotFoundError`` est déclenchée quand on essaie d'accéder à un fichier :quiz:`{"type":"SC","values":"introuvable,binaire,corrompu","answer":"introuvable"}` - :quiz:`{"type":"TF","answer":"T"}` Une ``FileNotFoundError`` est une exception d'exécution. - ``open('inexistant.txt', 'r')`` déclenche une exception de type :quiz:`{"type":"SC","values":"SyntaxError,FileNotFoundError,NameError","answer":"FileNotFoundError"}` - :quiz:`{"type":"TF","answer":"T"}` Les ressources externes peuvent ne pas être disponibles lors de l'exécution. La gestion des exceptions ------------------------- Après ce tour d'horizon des exceptions les plus courantes, voici le moyen de les gérer. La philosophie de Python n'est pas de contrôler en amont les conditions d'exécution du programme, ce qui peut être lourd, fastidieux et peu lisible mais de tenter d'exécuter l'opération en apportant une solution de repli au cas où elle ne fonctionnerait pas. Ceci s'effectue avec la construction ``try`` - ``except`` - ``else`` - ``finally``, similaire dans le principe au ``try-catch-finally`` de Java : #. on tente d'exécuter les instructions contenues dans le bloc ``try`` ; #. si aucune erreur n'est produite, on exécute ensuite les instructions contenues dans les blocs ``else`` puis ``finally`` ; #. sinon, on exécute les instructions contenues dans les blocs ``except`` puis ``finally``; .. quiz:: quiz-python-gestion :title: Gestion des exceptions en Python - Le bloc ``try`` contient les instructions à :quiz:`{"type":"SC","values":"jamais,parfois,toujours","answer":"toujours"}` exécuter. - :quiz:`{"type":"TF","answer":"T"}` Si une exception se produit dans ``try``, le bloc ``except`` est exécuté. - :quiz:`{"type":"TF","answer":"T"}` Le bloc ``finally`` est exécuté dans tous les cas. - :quiz:`{"type":"TF","answer":"T"}` Une exception est déclenchée quand une erreur se produit dans le bloc ``try`` - :quiz:`{"type":"TF","answer":"F"}` Une exception est déclenchée quand une erreur se produit dans le bloc ``except`` - :quiz:`{"type":"TF","answer":"F"}` Lorsqu'il n'y a pas d'erreur dans le bloc ``try``, le bloc ``except`` est exécuté - :quiz:`{"type":"TF","answer":"T"}` Lorsqu'il n'y a pas d'erreur dans le bloc ``try``, le bloc ``else`` est exécuté - :quiz:`{"type":"TF","answer":"T"}` Lorsqu'il n'y a pas d'erreur dans le bloc ``try``, le bloc ``finally`` est exécuté - :quiz:`{"type":"TF","answer":"T"}` Lorsqu'il y a une erreur dans le bloc ``try``, le bloc ``except`` est toujours exécuté - :quiz:`{"type":"TF","answer":"F"}` Lorsqu'il y a une erreur dans le bloc ``try``, le bloc ``else`` est toujours exécuté - :quiz:`{"type":"TF","answer":"T"}` Lorsqu'il y a une erreur dans le bloc ``try``, le bloc ``finally`` est toujoursexécuté .. exercice:: Division par zéro (sans IA) On considère le code suivant : .. code-block:: python :linenos: for res in range(3): print("Trying to compute 1 /",res) try: print(" 1/",res, "=", 1/res) except ZeroDivisionError: print(" 1 /",res,"tend vers l'infini") else: print(" Le résultat a une valeur finie") finally: print(' Fin de la division') Conjecturer le résultat de l'exécution. Il est nécessaire que VS Code soit démarré sur la machine hôte, pointe vers le répertoire ``e3-programmation-labs-student`` et que le container Docker soit lancé. - Créer un fichier ``exercices/divparzero.py`` et copier le code ci dessus ; - Exécuter le programme avec la commande ``python3 divparzero.py`` ; - Vérifier que le résultat affiché correspond à votre conjecture ; - Lorsque c'est le cas : - ``git add .`` - ``git commit -m "Division par zéro"`` - ``git push`` .. exercice:: Fichier inexistant (sans IA) On considère maintenant le code suivant : .. code-block:: python :linenos: for file in ["existing_file.txt", "non_existing_file.txt"]: try: print("Trying to open : ***", file, "***") f = open(file, 'r') print(" le fichier a été trouvé") print(" nombre de caractères :",len(f.read())) except FileNotFoundError: print(" le fichier n'a pas été trouvé") finally: f.close() print(" Fin de l'opération") Conjecturer le résultat de l'exécution. Il est nécessaire que VS Code soit démarré sur la machine hôte, pointe vers le répertoire ``e3-programmation-labs-student`` et que le container Docker soit lancé. - Créer un fichier ``exercices/existing_file.txt`` et un fichier ``exercices/fichier_inexistant.py`` et copier le code ci dessus ; - Exécuter le programme avec la commande ``python3 fichier_inexistant.py`` ; - Vérifier que le résultat affiché correspond à votre conjecture ; - Lorsque c'est le cas : - ``git add .`` - ``git commit -m "Fichier inexistant"`` - ``git push`` C'est une bonne pratique que d'encapsuler les opérations non sûres dans des blocs ``try`` - ``except`` - ``else`` - ``finally``. EAFP vs LBYL ------------ On peut également utiliser les exceptions pour simplifier le flux de traitement. Dans certains langages de programmation (C par exemple), on utilise le paradigme Look Before You Leap (LBYL) qui consiste à effectuer l'ensemble des tests nécessaires avant de déclencher une opération. Les langages comportant une gestion des exceptions peuvent mettre en oeuvre le paradigme Easier to Ask for Forgiveness than Permission (EAFP) dont le principe de base est de tenter l'opération et de réagir si elle échoue. LBYL .... Si on met en oeuvre le paradigme LBYL, on sécurise l'accès à une clé de dictionnaire avec le code suivant. .. code-block:: python if "key" in my_dict: x = my_dict["key"] else: handle_missing_key() En cas de programmation concurrente (multithreads), on peut donc aboutir à `une situation de compétition `_, conduisant un comportement non prédictible. Dans l'exemple qui précède, le dictionnaire peut être modifié entre le test et l'accès à la clé. EAFP .... Pour le paradigme EAFP... .. code-block:: python try: x = my_dict["key"] except KeyError: handle_missing_key() Lorsque l'erreur est peu probable, l'utilisation du paradigme EAFP conduit à de meilleures performances algorithmiques. .. quiz:: quiz-paradigmes :title: Paradigmes LBYL et EAFP - LBYL signifie :quiz:`{"type":"FB","answer":"Look Before You Leap","flags":"fuzzy"}` - EAFP signifie :quiz:`{"type":"FB","answer":"Easier to Ask for Forgiveness than Permission"}` - :quiz:`{"type":"TF","answer":"T"}` Le paradigme LBYL consiste à tester avant de déclencher une opération. - :quiz:`{"type":"TF","answer":"T"}` Le paradigme EAFP consiste à tenter une opération et réagir en cas d'erreur. - :quiz:`{"type":"TF","answer":"T"}` En cas de programmation concurrente, le paradigme EAFP est plus sûr que LBYL. Les exceptions sont des objets ------------------------------ En Python tout est objet, les exceptions ne font pas exception (!) à la règle. On considère le code suivant. .. code-block:: python >>> try: ... a = 1/0 ... except Exception as e: ... print(type(e)) ... print(dir(e)) ... [... 'args', 'with_traceback'] Ici la référence ``e`` est affectée à l'exception générée, ce qui permet de la manipuler ensuite. .. quiz:: quiz-python-objets :title: Les exceptions sont des objets - En Python, une exception est un :quiz:`{"type":"FB","answer":"objet"}` - :quiz:`{"type":"TF","answer":"T"}` On peut capturer une exception avec ``except ... as e``. - La variable ``e`` contient l'instance de l'exception :quiz:`{"type":"FB","answer":"généré","flags":"fuzzy"}` - :quiz:`{"type":"TF","answer":"T"}` On peut appeler ``type(e)`` sur une exception capturée. - La classe de base de toutes les exceptions est de type :quiz:`{"type":"SC","values":"Exception,ZeroDivisionError,str","answer":"Exception"}` .. exercice:: Conversion de type (sans IA) Il est nécessaire que VS Code soit démarré sur la machine hôte, pointe vers le répertoire ``e3-programmation-labs-student`` et que le container Docker soit lancé. Il est courant que les données numériques soient lues comme des chaines de caractères. Sans traitement, elles ne sont donc pas éligibles aux fonctions arithmétiques et statistiques élémentaires : ``sum()``, ``statistics.mean()``, ... Il faut alors effectuer une conversion vers le type numérique adapté lorsque c'est pertinent et ne rien faire si la donnée est effectivement une chaîne de caractères. Créer un fichier :file:`exercices/conversion.py` qui contient - une fonction secondaire :func:`parse` : - qui prend en argument une chaine de caractère ; - et retourne un nombre décimal, un entier ou une chaine de caractère selon son contenu. - le paradigme EAFP est une alternative au traditionnel LBYL et sera mis en oeuvre pour cette opération de conversion. On s'interrogera sur l'ordonnancement des constructions ``try`` à mettre en oeuvre. Du général au particulier ? Du particulier au général ? - une fonction principale :func:`main()` qui appelle la fonction :func:`parse` avec des arguments de nature différente et affiche les résultats. Vérifier le bon fonctionnement du programme. Une fois qu'il est opérationnel, synchroniser les repos : - ``git add .`` - ``git commit -m "Conversion de type"`` - ``git push``