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.

  • Une exception interrompt systématiquement l’exécution d’un programme

  • Une exception non gérée interrompt systématiquement l’exécution d’un programme

  • Une erreur de est détectée avant l’exécution du programme.

  • Les erreurs de logique génèrent des messages d’erreur peu explicites.

  • Les erreurs peuvent arrêter un programme.

  • Les erreurs de syntaxe sont détectées du programme.

  • On peut intercepter un problème d’exécution avant qu’il n’interrompe le programme

  • Il est obligatoire d’intercepter un problème d’exécution avant qu’il n’interrompe le programme

  • 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.

  • Le langage C possède un mécanisme d’exception.

  • La fonction perror() arrête le programme.

  • La gestion des erreurs en C repose sur des 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.

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.

  • Pour indiquer le succès, une fonction retourne généralement

  • Une valeur indique une erreur.

  • Vérifier le code de retour d’une fonction est une bonne pratique.

  • 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.

FILE *f = fopen("test.txt", "r");

if (f == NULL)
{
    perror("fopen");
}

Ici, l’erreur est séparée du code de retour.

  • La variable errno est renseignée par fonctions de la bibliothèque standard

  • errno est une variable globale.

  • La fonction perror() affiche un message d’erreur correspondant à

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.

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");
}
  • En C, pour retourner plusieurs valeurs, on peut utiliser des

  • Un paramètre de sortie permet de retourner le résultat d’une fonction.

  • Dans l’exemple, si b vaut 0, la fonction retourne

  • En cas d’erreur la fonction division retourne

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.

#include <assert.h>
#include <stdlib.h>

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.

  • La fonction abort() le programme

  • assert() vérifie une condition.

  • Si une assertion échoue, abort() est automatiquement appelée.

  • Les assertions sont souvent utilisées pour vérifier les d’une fonction.

  • Les assertions sont vérifiées

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.

  • En Python, une exception confirme la règle

  • Les exceptions en Python sont des

  • Une exception interrompt le cours normal du programme.

  • Gérer une exception permet de l’exécution du programme après capture de l’erreur.

  • Une exception est déclenchée du programme.

Une vidéo de présentation des exceptions…

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 SyntaxError.

>>> if x%2 == 0 print('x est pair')
  File "<stdin>", line 1
    if x%2 == 0 print('x est pair')
                    ^
SyntaxError: invalid syntax

Une exception de type IndentationError se produit lorsque l’on indente mal son programme.

>>> if True:
...     print('ligne indentée avec une tabulation')
...     print('ligne indentée avec 4 espaces')
  File "<stdin>", 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.

  • Une erreur de syntaxe déclenche une exception de type

  • Python détecte les erreurs de syntaxe pendant l’exécution.

  • Une IndentationError est déclenchée quand on un programme.

  • Pour éviter les erreurs d’indentation, on peut remplacer le caractère par

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 NameError.

>>> if x%2 == 0: print('x est pair')
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

Il peut également se produire un problème lors de l’importation d’un module.

>>> import un_module_qui_nexiste_pas
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named 'un_module_qui_nexiste_pas'

ou lorsqu’on applique une fonction a un objet inapproprié.

>>> len(True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'bool' has no len()
  • Une exception NameError est déclenchée quand on utilise une variable

  • Une NameError est une erreur de syntaxe.

  • Une ImportError est déclenchée lors de l’échec de l’import d’un

  • Une TypeError est déclenchée quand on utilise un objet inapproprié.

  • 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.

>>> res = 0
>>> 1/res
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

ou dépassant les capacités de la machine…

>>> 2.0**4000
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OverflowError: (34, 'Result too large')
  • Une ZeroDivisionError est déclenchée lors d’une

  • 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

  • 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 FileNotFoundError est déclenchée.

>>> open('un_fichier_qui_nexiste_pas.txt', 'r')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'un_fichier_qui_nexiste_pas.txt'
  • Une FileNotFoundError est déclenchée quand on essaie d’accéder à un fichier

  • Une FileNotFoundError est une exception d’exécution.

  • open('inexistant.txt', 'r') déclenche une exception de type

  • 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 :

  1. on tente d’exécuter les instructions contenues dans le bloc try ;

  2. si aucune erreur n’est produite, on exécute ensuite les instructions contenues dans les blocs else puis finally ;

  3. sinon, on exécute les instructions contenues dans les blocs except puis finally;

  • Le bloc try contient les instructions à exécuter.

  • Si une exception se produit dans try, le bloc except est exécuté.

  • Le bloc finally est exécuté dans tous les cas.

  • Une exception est déclenchée quand une erreur se produit dans le bloc try

  • Une exception est déclenchée quand une erreur se produit dans le bloc except

  • Lorsqu’il n’y a pas d’erreur dans le bloc try, le bloc except est exécuté

  • Lorsqu’il n’y a pas d’erreur dans le bloc try, le bloc else est exécuté

  • Lorsqu’il n’y a pas d’erreur dans le bloc try, le bloc finally est exécuté

  • Lorsqu’il y a une erreur dans le bloc try, le bloc except est toujours exécuté

  • Lorsqu’il y a une erreur dans le bloc try, le bloc else est toujours exécuté

  • Lorsqu’il y a une erreur dans le bloc try, le bloc finally est toujoursexécuté

Division par zéro (sans IA)

On considère le code suivant :

 1for res in range(3):
 2    print("Trying to compute 1 /",res)
 3    try:
 4        print("   1/",res, "=", 1/res)
 5    except ZeroDivisionError:
 6        print("   1 /",res,"tend vers l'infini")
 7    else:
 8        print("   Le résultat a une valeur finie")
 9    finally:
10        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

Fichier inexistant (sans IA)

On considère maintenant le code suivant :

 1for file in ["existing_file.txt", "non_existing_file.txt"]:
 2    try:
 3        print("Trying to open : ***", file, "***")
 4        f = open(file, 'r')
 5        print("   le fichier a été trouvé")
 6        print("   nombre de caractères :",len(f.read()))
 7    except FileNotFoundError:
 8        print("   le fichier n'a pas été trouvé")
 9    finally:
10        f.close()
11        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.

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…

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.

  • LBYL signifie

  • EAFP signifie

  • Le paradigme LBYL consiste à tester avant de déclencher une opération.

  • Le paradigme EAFP consiste à tenter une opération et réagir en cas d’erreur.

  • 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.

>>> try:
...     a = 1/0
... except Exception as e:
...    print(type(e))
...    print(dir(e))
...
<class 'ZeroDivisionError'>
[... '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.

  • En Python, une exception est un

  • On peut capturer une exception avec except ... as e.

  • La variable e contient l’instance de l’exception

  • On peut appeler type(e) sur une exception capturée.

  • La classe de base de toutes les exceptions est de type

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 exercices/conversion.py qui contient

  • une fonction secondaire 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 main() qui appelle la fonction 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