Maîtrise des exceptions en Python : détection, traitement et création

La gestion des erreurs est un aspect fondamental de la programmation robuste. En Python, cela se fait principalement via le mécanisme des exceptions, qui permet de signaler et de traiter les conidtions anormales survenues lors de l'exécution d'un programme.

Concepts fondamentaux : erreurs vs exceptions

Une erreur de syntaxe empêche l'interpréteur de comprendre le code et doit être corrigée avant l'exécution. Une erreur logique conduit à un résultat incorrect ou à un comportement imprévu. Une exception, en revanche, survient à l'exécution lorsque Python rencontre une situation qu'il ne peut pas gérer normalement (comme une division par zéro). L'interprète interrompt alors le flux d'exécution normal et recherche un gestionnaire d'exception approprié.

La structure de base : le bloc try-except

Le mécanisme de base pour intercepter et gérer les exceptions est le bloc try...except. Le code susceptible de générer une exception est placé dans le bloc try. Si une exception se produit, l'exécution saute immédiatement au bloc except correspondant au type d'exception levée.

def lire_entier_positif():
    while True:
        try:
            saisie = input("Veuillez entrer un entier positif : ")
            valeur = int(saisie)
            if valeur <= 0:
                raise ValueError("La valeur doit être positive")
            return valeur
        except ValueError as details:
            print(f"Saisie invalide : {details}. Réessayez.")

Cet exemple illustre plusieurs points clés : l'interception d'une exception prédéfinie (ValueError), l'utilisation d'une clause as pour capturer l'objet exception, et la levée explicite d'une exception (raise) avec un message personnalisé pour des contraintes métier.

Précision et flexibilité avec les clauses multiples

On peut gérer différents types d'exceptions dans des blocs except distincts pour appliquer une logique spécifique à chaque situation d'erreur. L'ordre des clauses est important : Python teste les exceptions dans l'ordre où elles sont listées.

def analyser_donnees(chemin_fichier):
    try:
        with open(chemin_fichier, 'r') as f:
            contenu = f.read()
            nombre = int(contenu.strip())
            resultat = 1000 / nombre
            return resultat
    except FileNotFoundError as err_fichier:
        print(f"ERREUR : Le fichier spécifié est introuvable. ({err_fichier})")
    except (ValueError, TypeError):
        print("ERREUR : Le contenu du fichier n'est pas un nombre entier valide.")
    except ZeroDivisionError:
        print("ERREUR : Le nombre contenu dans le fichier est zéro.")
    except Exception as e:
        print(f"Une erreur inattendue est survenue : {type(e).__name__}")

La clause except Exception sert de filet de sécurité pour toute exception non anticipée. On peut aussi regrouper plusieurs types d'exceptions dans un tuple pour leur appliquer un même traitement.

Assurer le nettoyage : la clause finally

La clause finally définit un bloc de code qui s'exécute systématiquement, qu'une exception ait été levée, interceptée ou non. C'est l'endroit idéal pour libérer des ressources critiques comme la fermeture de fichiers ou de connexions réseau.

connexion = None
try:
    connexion = obtenir_connexion_bd()
    curseur = connexion.execute("SELECT * FROM donnees_sensibles")
    traiter_resultats(curseur)
except ErreurBaseDeDonnees as e:
    journal_logger(f"Échec de la requête : {e}")
finally:
    if connexion:
        connexion.deconnecter()
        print("Connexion à la base de données libérée.")

Exécution conditionnelle avec la clause else

La clause else est exécutée seulement si le bloc try s'est terminé sans aucune exception. Cela permet de séparer clairement le code "normal" du code de gestion d'erreur et du code de nettoyage.

def calculer_moyenne(valeurs):
    total = 0
    compteur = 0
    try:
        for element in valeurs:
            total += float(element)
            compteur += 1
    except (TypeError, ValueError) as e:
        print(f"Impossible de convertir '{element}' en nombre : {e}")
        return None
    else:
        if compteur == 0:
            print("Aucune valeur valide fournie.")
            return None
        return total / compteur

Provoquer des exceptions avec raise

En plus d'intercepter les exceptions, un programme peut en déclencher explicitement avec l'instruction raise. Cela est utile pour signaler une condition d'erreur spécifique dans votre code ou pour propager une exception après avoir effectué une certaine journalisation.

def definir_limite(nouvelle_limite):
    if not isinstance(nouvelle_limite, int):
        raise TypeError("La limite doit être un entier.")
    if nouvelle_limite < 0:
        raise ValueError("La limite ne peut pas être négative.")
    # ... logique d'application de la limite

Vérifications préalables avec les assertions

L'instruction assert est conçue pour les vérifications de débogage et les invariants de programme. Elle lève une AssertionError si la condition testée est fausse.

def diviser(a, b):
    assert isinstance(a, (int, float)), "Le dividende doit être numérique"
    assert isinstance(b, (int, float)), "Le diviseur doit être numérique"
    assert b != 0, "Division par zéro impossible"
    return a / b

Attention : les assertions peuvent être désactivées avec l'option -O de l'interpréteur Python. Ne les utilisez donc pas pour la validation des données provenant de l'utilisateur ou d'une source externe.

Hiérarchie des exceptions intégrées

Toutes les exceptions natives de Python héritent de BaseException. Les exceptions les plus courantes (Exception, ArithmeticError, OSError, etc.) descendent de la sous-classe Exception. Cette hiérarchie permet de capturer des catégories entières d'erreurs. Par exemple, except ArithmeticError interceptera ZeroDivisionError, OverflowError, etc.

Créer des exceptions personnalisées

Pour une gestion plus fine des erreurs dans vos applications, vous pouvez définir vos propres hiérarchies d'exceptions. Il est recommandé de faire hériter vos exceptions personnalisées de Exception.

class ErreurConfiguration(Exception):
    """Classe de base pour les erreurs de configuration."""
    pass

class FichierConfigManquant(ErreurConfiguration):
    """Le fichier de configuration principal est absent."""
    def __init__(self, chemin):
        self.chemin = chemin
        super().__init__(f"Fichier de configuration introuvable : {chemin}")

class ValeurConfigInvalide(ErreurConfiguration):
    """Une valeur dans la configuration est incorrecte."""
    def __init__(self, cle, valeur, raison):
        self.cle = cle
        self.valeur = valeur
        self.raison = raison
        super().__init__(f"Valeur invalide pour '{cle}': '{valeur}' - {raison}")

Diagnostic avancé avec sys.exc_info()

La fonction sys.exc_info() retourne un tuple de trois éléments contenant des informations détaillées sur l'exception en cours de traitement : le type de l'exception, l'instance de l'exception, et un objet traceback décrivant la pile d'appels au moment de l'erreur.

import sys
import traceback

def traiter_erreur_inconnue():
    try:
        operation_complexe()
    except:
        type_exc, valeur_exc, tb_objet = sys.exc_info()
        print(f"Type : {type_exc.__name__}")
        print(f"Valeur : {valeur_exc}")
        # On peut parcourir l'objet traceback pour des détails précis
        lignes_tb = traceback.format_tb(tb_objet)
        print("Traceback :")
        for ligne in lignes_tb:
            print(ligne.strip())

Exemple pratique : script de conversion robuste

Intégrons ces concepts dans un script concret qui lit un fichier, effectue un calcul et gère proprement les erreurs courantes.

#!/usr/bin/env python3
import sys
import os

def extraire_et_calculer(chemin_entree):
    """Lit un nombre depuis un fichier et calcule son inverse."""
    # Validation initiale du chemin
    if not os.path.isfile(chemin_entree):
        raise FileNotFoundError(f"Fichier non trouvé : {chemin_entree}")

    try:
        with open(chemin_entree, 'r') as fichier_entree:
            contenu = fichier_entree.read().strip()
            if not contenu:
                raise ValueError("Le fichier est vide.")
            
            nombre = int(contenu)
            resultat = 1.0 / nombre
            return resultat

    except ValueError as e:
        # Convertit l'erreur en une exception de notre domaine
        raise ErreurTraitement(f"Contenu invalide dans '{chemin_entree}': {e}") from e
    except ZeroDivisionError:
        raise ErreurTraitement(f"La valeur dans '{chemin_entree}' est zéro.")
    except IOError as e:
        raise ErreurTraitement(f"Erreur d'E/S sur '{chemin_entree}': {e}") from e

class ErreurTraitement(Exception):
    """Exception spécifique aux erreurs de traitement des données."""
    pass

# Point d'entrée principal
if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Utilisation : python script.py <fichier_donnees>", file=sys.stderr)
        sys.exit(1)
    
    fichier = sys.argv[1]
    try:
        valeur = extraire_et_calculer(fichier)
        print(f"Résultat du calcul : {valeur}")
    except (FileNotFoundError, ErreurTraitement) as e:
        print(f"ERREUR : {e}", file=sys.stderr)
        sys.exit(1)
</fichier_donnees>

Ce script démontre une approche structurée : validation des entrées, conversion des exceptions bas niveau en exceptions métier informatives, et gestion centralisée des erreurs au point d'entrée.

Étiquettes: Python Exception try-except Raise AssertionError

Publié le 6 juillet à 01h46