Exceptions and assertions

Reading time10 min

En bref

Résumé de l’article

Dans ce cours, nous introduisons les exceptions, qui indiquent des erreurs détectées lors de l’exécution du programme.

Nous détaillons également les assertions, qui permettent de faire certaines vérifications à un moment donné du programme.
Elles sont très pratiques pour le développement, car elles permettent de déclencher des erreurs lorsqu’un problème survient.

Points clés

  • Les exceptions sont utilisées pour gérer les erreurs qui peuvent survenir lors de l’exécution du programme, telles que des entrées utilisateur invalides ou des fichiers manquants.

  • Les exceptions peuvent être interceptées et gérées par le programme.

  • Le bloc try-except est utilisé pour intercepter et gérer les exceptions en Python.

  • La clause finally est utilisée pour définir des actions de nettoyage qui doivent être exécutées dans tous les cas.

  • Les assertions sont faites pour valider et appliquer des conditions attendues dans le code pendant l’exécution.

  • Les assertions échouées fournissent un retour immédiat, aidant les développeurs à identifier et corriger rapidement les problèmes.

  • L’utilisation cohérente des assertions impose des contraintes, conduisant à un logiciel plus robuste et fiable.

  • Les assertions peuvent être activées/désactivées facilement en production.

Contenu de l’article

1 — Exceptions

1.1 — Qu’est-ce qu’une exception ?

Les exceptions sont utilisées pour gérer des erreurs qui peuvent survenir même si le code est parfaitement correct.
Voici quelques exemples de tels scénarios :

  • Un utilisateur saisit des données invalides.
  • Un fichier est manquant.
  • Une connexion réseau est perdue.

Une exception est un événement qui survient pendant l’exécution d’un programme, qui perturbe le flux normal des instructions.
Mais les exceptions ne sont pas nécessairement fatales, elles peuvent être gérées par le programme.

Cependant, lorsqu’une exception n’est pas gérée par les programmes, cela entraîne un message d’erreur comme celui montré ci-dessous :

def euclidean_division (a: int, b: int) -> int:

    """
        This function returns the integer division of a by b.
        Beware of division by 0!
        In:
            * a: The numerator.
            * b: The divisor.
        Out:
            * The integer division of a by b.
    """

    # No check for division by 0, the program will crash if b is 0
    return a // b
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This function returns the integer division of a by b.
     * Beware of division by 0!
     *
     * @param a The numerator.
     * @param b The divisor.
     * @return  The integer result of dividing a by b.
     */
    public static int euclideanDivision(int a, int b){ 
        // No check for division by 0, the program will crash if b is 0
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(euclideanDivision(10, 0));
    }

}

L’exécution du code précédent avec b = 0 déclenchera une ZeroDivisionError en Python (ArithmeticException en Java).

Sortie
Traceback (most recent call last):
  File "functions.py", line 16, in <module>
    euclidean_division(10, 0) 
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "functions.py", line 14, in euclidean_division
    return a // b
           ~~^^~~
ZeroDivisionError: integer division or modulo by zero
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at org.step1.Main.euclideanDivision(Mai.java:30)
	at org.step1.Main.main(Mai.java:34)

La dernière ligne du message d’erreur indique ce qui s’est passé.
Les exceptions existent sous différents types, et le type est affiché dans le message (ZeroDivisionError et ArithmeticException).
La chaîne affichée comme type d’exception est le nom de l’exception intégrée qui s’est produite.

Information

Les programmes peuvent définir leurs propres exceptions en créant une nouvelle classe d’exception (qui doit dériver de la classe Exception), mais nous ne décrirons pas cela dans cette leçon.

1.2 — Gestion des exceptions

1.2.1 — Le bloc try-except

Il est possible d’écrire des programmes qui gèrent certaines exceptions, c’est-à-dire qui détectent quand une exception survient, et la capturent pour faire quelque chose et empêcher le programme de planter.
Pour cela, on peut utiliser un bloc try-except pour intercepter et gérer l’exception.
La syntaxe du bloc try-except est la suivante :

# The code that has a chance to raise an exception should be written within the try block
try:
    <do something>

# The code to execute in case of an exception of specified type should be written in the except block
except <exception type>:
    <handle the error>

L’instruction try fonctionne comme suit :

  1. D’abord, la clause try (l’instruction ou les instructions entre les mots-clés try et except) est exécutée.
  2. Voici ce qui peut se passer ensuite :
    • Si aucune exception ne survient, la clause except est ignorée, et l’exécution de la clause try est terminée.
    • Si une exception survient pendant l’exécution de la clause try (par exemple, à la ligne 42), le reste de la clause (lignes 43 et suivantes) est ignoré.
      Ensuite, si le type de l’exception levée correspond à l’exception nommée après le mot-clé except (<exception type> dans le code ci-dessus), la clause except est exécutée, puis l’exécution continue après le bloc try-except.
    • Si une exception survient qui ne correspond pas à l’exception nommée dans la clause except, elle est transmise aux instructions try externes (s’il y en a).
      Si aucun gestionnaire n’est trouvé, c’est une exception non gérée et l’exécution s’arrête avec un message d’erreur.

L’exemple suivant demande à l’utilisateur une entrée jusqu’à ce qu’un entier valide soit saisi, mais permet à l’utilisateur d’interrompre le programme (en utilisant Control-C).

# Infinite loop
while True:

    # Start the block that may raise an exception
    try:

        # Here, int() can raise a ValueError if the provided string cannot be converted to an integer
        x = int(input("Please enter a number: "))

        # If we can reach this point, it means that no exception was raised
        # We abort the infinite loop
        break

    # In case of a ValueError, we make a print and loop again
    except ValueError:
        print("Oops!  That was no valid number.  Try again...")

Une instruction try peut avoir plus d’une clause except, pour spécifier des gestionnaires pour différentes exceptions.
Au plus un gestionnaire sera exécuté.
Une clause except peut nommer plusieurs exceptions sous forme d’un tuple entre parenthèses, par exemple :

except (RuntimeError, TypeError, NameError):
    pass

L’instruction pass est utilisée comme un espace réservé pour du code futur.
Lorsque l’instruction pass est exécutée, rien ne se passe (c’est une commande pour “ne rien faire”), mais cela évite d’obtenir une erreur lorsque du code vide n’est pas autorisé.
Le code vide n’est pas autorisé dans les boucles, définitions de fonctions, définitions de classes, ou dans les instructions if.

Information

Comme toutes les exceptions Python dérivent d’une classe de base appelée Exception, vous pouvez intercepter toute erreur qui survient pendant le bloc try en écrivant :

except Exception:
    pass

Ou, de manière équivalente :

except:
    pass

Enfin, si vous souhaitez plus d’informations sur l’exception que vous avez capturée, vous pouvez utiliser la bibliothèque traceback, et obtenir l’exception dans une variable (e dans le code ci-dessous) :

# Needed import
import traceback

# Raise and catch an exception
try
    x = 1 / 0
except Exception as e:

    # Print a few info
    print(f"Type of the exception: {type(e).__name__}")
    print(f"Error message of the exception: {str(e)}")
    
    # Show complete traceback (which function was called, etc.)
    print("Traceback:")
    traceback.print_exc()
1.2.2 — Définir des actions de nettoyage

L’instruction try a une autre clause optionnelle destinée à définir des actions de nettoyage qui doivent être exécutées dans tous les cas.
Si une clause finally est présente, la clause finally s’exécutera en dernière tâche avant que l’instruction try ne se termine.
Par exemple :

try:
    while True:
        print("Program is running")

except KeyboardInterrupt:
    print("Oh! you pressed CTRL + C.")
    print("Program interrupted.")

finally:
    print("This was an important code, ran at the end.")
Sortie
Program is running
Program is running
...
Program is running
Program is running
Prog^Cis running
Oh! you pressed CTRL + C.
Program interrupted.
This was an important code, ran at the end.
Information

La clause finally s’exécute que l’instruction try produise ou non une exception.

1.3 — Lever des exceptions

Les exceptions sont levées lorsqu’une erreur survient.
Mais il est aussi possible pour un programmeur de forcer une exception à se produire avec le mot-clé raise.
L’argument de raise indique l’exception à lever.
Cela doit être soit une instance d’exception, soit une classe d’exception.

Par exemple :

def euclidean_division (a: int, b: int) -> int:

    """
        This function returns the integer division of a by b.
        Beware of division by 0!
        In:
            * a: The numerator.
            * b: The divisor.
        Out:
            * The integer division of a by b.
        Exceptions:
            * ZeroDivisionError: If b is 0.
    """

    # Raise an exception with a custom error message
    if b == 0:
        raise ZeroDivisionError("Division by zero. Please provide a non-zero divisor.")
    
    # Perform a division that cannot be by 0
    return a // b
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This function returns the integer division of a by b.
     * Beware of division by 0!
     *
     * @param a The numerator.
     * @param b The divisor.
     * @return  The integer result of dividing a by b.
     * @throws ArithmeticException If b is 0.
     */
    public static int euclideanDivision(int a, int b) throws ArithmeticException {
        // Raise an exception with a custom error message
        if (b == 0) {
            throw new ArithmeticException("Division by zero. Please provide a non-zero divisor.");
        } 
        // Perform a division that cannot be by 0
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(euclideanDivision(10, 0));
    }

}

Dans l’exemple ci-dessus, la fonction euclidean_division lève une ZeroDivisionError en Python (et une ArithmeticException en Java) si le diviseur b est 0.
Ici, le même type d’erreur est levé mais le message diffère.
Cela montre principalement comment lever une exception.

Nous pourrions aller plus loin et définir notre propre classe étendant Exception et la lever, au lieu de simplement personnaliser le message d’erreur.
Avec cela, nous pourrions ajouter des propriétés à l’objet exception créé et les utiliser dans le bloc except.

2 — Assertions

Les assertions sont un outil crucial dans le développement logiciel utilisé pour valider les hypothèses faites dans le code.
Ce sont des types spécifiques d’exceptions, faites pour le développement.
Les assertions vérifient si des conditions spécifiques sont vraies pendant l’exécution, aidant à détecter tôt les bugs et erreurs logiques.

Lorsqu’une assertion échoue, elle fournit un retour immédiat, aidant à un débogage rapide.
En imposant des contraintes et des comportements attendus, les assertions contribuent à créer un code plus fiable et maintenable.

2.1 — Qu’est-ce qu’une assertion ?

Les assertions nous donnent un moyen de rendre nos hypothèses explicites dans notre code.
Elles sont utilisées pour vérifier que l’état du programme est conforme aux attentes à un point donné du code.
Si l’assertion échoue, une exception est levée, et le programme s’arrête.
Ainsi, le mécanisme d’assertion est utilisé pour vérifier des conditions pendant l’exécution du code.

La plupart du temps, cela ne nécessite aucune dépendance supplémentaire.
En Python (comme en Java), une assertion est ajoutée en utilisant le mot réservé assert, suivi de la condition à vérifier, et d’un message optionnel à afficher si l’assertion échoue : assert condition, message.

Considérons la fonction euclidean_division vue précédemment.
Dans ce code, nous avons une hypothèse que b != 0.
Par conséquent, une assertion peut être plus adaptée qu’une exception :

def euclidean_division (a: int, b: int) -> int:

    """
        This function returns the integer division of a by b.
        Beware of division by 0!
        In:
            * a: The numerator.
            * b: The divisor.
        Out:
            * The integer division of a by b.
    """

    # This assertion will fail when a division by 0 is attempted
    assert b != 0, "Division by zero"

    # If we pass all assertions, we are fine
    return a // b
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This function returns the integer division of a by b.
     * Beware of division by 0!
     *
     * @param a The numerator.
     * @param b The divisor.
     * @return  The integer result of dividing a by b.
     */
    public static int euclideanDivision(int a, int b){ 
        // This assertion will fail when a division by 0 is attempted
        assert b !=0 : "Division by zero";

        // If we pass all assertions, we are fine
        return a / b;
    }

}

Les assertions peuvent être utilisées pour enrichir le code en vérifiant des invariants de code et ainsi être vues comme une forme de documentation : elles peuvent décrire l’état que le code s’attend à trouver avant son exécution.

Cependant, les assertions sont uniquement utilisées en phase de développement et sont rendues silencieuses en production.
Ainsi, en production, la vérification que b != 0 ci-dessus n’est pas effectuée.
On suppose donc que le programme ne devrait jamais appeler la fonction avec ce cas problématique.

Information

Cela est fait pour accélérer le code et éviter des vérifications inutiles.

Cependant, si vous pensez que l’erreur peut encore se produire en production, une exception peut être plus adaptée.
C’est particulièrement le cas lorsque vous interagissez avec un utilisateur qui pourrait définir b = 0.

Comme les assertions peuvent être désactivées, vous devez faire attention aux points suivants :

  • Les assertions ne doivent pas avoir d’effets de bord (elles ne doivent pas modifier l’état du programme, mais seulement le vérifier) – Par exemple, vous ne devez pas utiliser une assertion pour ouvrir un fichier, mettre à jour une structure de données, etc.

  • Les assertions ne doivent pas être utilisées pour vérifier des conditions qui peuvent être causées par des facteurs externes (par exemple, saisie utilisateur, réseau, etc.) – Par exemple, vous ne devez pas utiliser d’assertions pour vérifier si un fichier existe.

  • Les assertions ne remplacent pas la gestion des erreurs, la validation des entrées, les tests, la documentation, etc.

  • Lors de l’utilisation d’assertions qui nécessitent un temps d’exécution assez élevé, assurez-vous que les calculs sont faits dans la condition assert.
    En effet, si vous calculez le résultat avant, le stockez dans une variable, puis vérifiez la variable dans l’assertion, le calcul sera quand même effectué même si les assertions sont désactivées.
    Pour de telles vérifications complexes, il peut donc être judicieux de définir des fonctions, et de les appeler dans l’assertion.
    Par exemple, dans le code suivant, la fonction is_sorted est appelée dans l’assertion :

def binary_search (data: list[int], search_value: int) -> int :

    """ 
        This function searches for a value in a sorted list using the binary search algorithm.
        In:
            * data: The list to search in.
            * search_value: The value to search for.
        Out:
            * The index of the value in the list if it is found, -1 otherwise.
    """

    assert len(data) > 0, "The list is empty"
    assert is_sorted(data), "The list is not sorted"    

Pour aller plus loin

On dirait que cette section est vide !

Y a-t-il quelque chose que vous auriez aimé voir ici ?
Faites-le nous savoir sur le serveur Discord !
Peut-être que nous pourrons l’ajouter rapidement.
Sinon, cela nous aidera à améliorer le cours pour l’année prochaine !

Pour aller au-delà

On dirait que cette section est vide !

Y a-t-il quelque chose que vous auriez aimé voir ici ?
Faites-le nous savoir sur le serveur Discord !
Peut-être que nous pourrons l’ajouter rapidement.
Sinon, cela nous aidera à améliorer le cours pour l’année prochaine !