Exceptions et assertions en python

Améliorez la qualité de vos implémentations en exploitant les exceptions et les assertions.

La gestion des exceptions est un concept bien souvent ignoré, volontairement ou non, par les débutants. Cela peut se concevoir dans le cadre d’une activité récréative, mais dans un contexte professionnel ou universitaire, la chose devient indispensable. Outre le fait de montrer qu’on maîtrise son code, la gestion des exceptions tient un rôle essentiel dans l’organisation du code. Des exceptions bien identifiées, interceptées et retranscrites à l’utilisateur permettront une meilleure qualité d'implémentation, une maintenance plus aisée du programme et bien sûr un support à l'utilisateur plus productif.

Cela étant dit, la gestion des exceptions peut, au premier abord, paraître assez indigeste. Il est vrai que certains concepts doivent être acquis d’une part et que, plus qu’ailleurs, les bonnes pratiques sont fortement recommandées.

Commençons par un exemple relativement simple :


def est_divisible(candidat: int, diviseur: int) -> bool:
   if candidat == 0:
       return True
   else:
       return candidat % diviseur == 0

La fonction ci-dessus a pour objet de retourner Vrai si le candidat passé en paramètre est divisible par le diviseur, également passé en paramètre. Dans le cas contraire, la fonction va retourner Faux.
La fonction étant relativement simple, vous avez déjà surement compris qu’un diviseur initialisé à 0 entrainera la levée d’un vilain message de type “ZeroDivisionError: integer division or modulo by zero”. Nous pouvons donc gérer cette exception pour éviter au programme de planter et laisser ainsi la main à l'utilisateur.

La première approche va être d’encapsuler notre code entre les instructions try et except, comme ceci :


def est_divisible(candidat: int, diviseur: int) -> bool:
   try:
       if candidat == 0:
           return True
       else:
           return candidat % diviseur == 0
   except ZeroDivisionError as e:
       print("Le diviseur ne peut etre 0 !)

Le code de notre fonction reste inchangé, cependant cette fois l’exception levée par l’instruction candidat % diviseur va être interceptée par le bloc except qui, au lieu d'éditer la sortie brute de l’erreur, va se contenter de suivre nos instructions. Ici nous éditons simplement un message mais, dans un programme plus complexe, nous aurions tout aussi bien pu implémenter une autre alternative.

Nous avons dans le cas de figure précédent adopter une approche passive qui consiste à tenter de dérouler notre programme et, dans le cas où une chose se passe mal, d’agir en conséquence. Cette approche est tout à fait pertinente dans des fonctions courtes et spécialisées.
La seconde approche est plus volontaire, elle consiste à lever soi-même les exceptions sur la base de conditions ou d’assertion. Elle a le mérite, d’une part de pouvoir distinguer pour un même type exception divers sources et, d’autre part, de générer des exceptions non problématiques techniquement parlant mais identifiées comme tel pour les besoins du projet. Voici un exemple :


def annee_correcte(annee: int) -> bool:
   try:
       if annee < 1960:
           raise ValueError('Annee inferieure a 1960')
       elif annee > 2000:
           raise ValueError('Annee superieure a 2000')
       else:
           return True
   except ValueError as e:
       print(e)

Ici encore le code est relativement simple, nous voulons simplement contrôler que l'année passée en paramètre de cette fonction soit bien comprise entre 1960 et 2000. Pour ce faire nous effectuons des tests en comparant l'année aux bornes 1960 puis 2000. Si une de ces conditions s'avère vraie, nous générons une exception de type “ValueError” en indiquant un message explicite.
Comme nous le voyons, le fait que l'année soit inférieure à 1960 ne pose pas de problème technique en soit, le programme ne va pas planter. Néanmoins, fonctionnellement parlant nous considérons que c’est un problème et générer un message de la sorte peut être une solution.

Les assertions

Nous aurions pu écrire le code précédent de la sorte :


def annee_correcte(annee: int) -> bool:
   try:
       assert annee >= 1960, 'Annee inferieure a 1960'
       assert annee <= 2000, 'Annee superieure a 2000'
       return True
   except AssertionError as e:
       print(e)

Bien qu’elle fasse exactement la même chose, cette version a l’air plus condensée. Elle est également plus lisible. Nous avons utilisé ici l’instruction “assert” qui permet de vérifier qu’un test retourne Vrai, dans le cas contraire une exception de type “AssertionError” est levée. Quels sont les avantages de cette instruction ?
Elle est utile pour tester les paramètres d'entrée des fonctions. Comme vous le voyez, en l’utilisant, nous avons regroupé nos tests en début de code et nous nous sommes abstenus d’utiliser des instructions “if…” qui, il est vrai, peuvent conduire rapidement à un certain nombre d'imbrications nuisant à la lisibilité du code.
Cette fonction peut également servir à dérouler des tests unitaires. Il s’agit là d'un autre sujet que l’on traitera dans un article indépendant.

Spécialisez vos exceptions

Certaines bonnes pratiques sont à privilégier en matière d’exception. La plus importante d’entre elles consiste à être le plus spécifique possible. En effet la plupart des exceptions sont des classes héritées de la classe “Exception”, rien ne nous empêche par exemple de spécifier systématiquement la levée d’exception de type “Exception”.
Néanmoins ce faisant nous aurions deux problèmes :

  • les exceptions plus spécifiques à suivre, si elles existent, ne seront pas levées,
  • ou au contraire nous aurons l’obligation de capter toutes les exceptions spécifiques en amont avant de capter enfin l’exception générique.

Voici un lien vers les différentes classes d’exception existantes :
https://docs.python.org/fr/3/library/exceptions.html#exception-hierarchy