Dans ce chapitre, nous aborderons le dernier concept que je considère comme indispensable avant d'attaquer la partie sur la Programmation Orientée Objet, j'ai nommé « les exceptions ».
Comme vous allez le voir, il s'agit des erreurs que peut rencontrer Python en exécutant votre programme. Ces erreurs peuvent être interceptées très facilement et c'est même, dans certains cas, indispensable.
Cependant, il ne faut pas tout intercepter non plus : si Python envoie une erreur, c'est qu'il y a une raison. Si vous ignorez une erreur, vous risquez d'avoir des résultats très étranges dans votre programme.
Nous avons déjà été confrontés à des erreurs dans nos programmes, certaines que j'ai volontairement provoquées, mais la plupart que vous avez dû rencontrer si vous avez testé un minimum des instructions dans l'interpréteur. Quand Python rencontre une erreur dans votre code, il lève une exception. Sans le savoir, vous avez donc déjà vu des exceptions levées par Python :
>>> # Exemple classique : test d'une division par zéro
>>> variable = 1/0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: int division or modulo by zero
Attardons-nous sur la dernière ligne. Nous y trouvons deux informations :
ZeroDivisionError : le type de l'exception ;
int division or modulo by zero : le message qu'envoie Python pour vous aider à comprendre l'erreur qui vient de se produire.
Python lève donc des exceptions quand il trouve une erreur, soit dans le code (une erreur de syntaxe, par exemple), soit dans l'opération que vous lui demandez de faire.
Notez qu'à l'instar des variables, on trouve différents types d'exceptions que Python va utiliser en fonction de la situation. Le type d'exception ValueError, notamment, pourra être levé par Python face à diverses erreurs de « valeurs ». Dans ce cas, c'est donc le message qui vous indique plus clairement le problème. Nous verrons dans la prochaine partie, consacrée à la Programmation Orientée Objet, ce que sont réellement ces types d'exceptions.
Bon, c'est bien joli d'avoir cette exception. On voit le fichier et la ligne à laquelle s'est produite l'erreur (très pratique quand on commence à travailler sur un projet) et on a une indication sur le problème qui suffit en général à le régler. Mais Python permet quelque chose de bien plus pratique.
Admettons que certaines erreurs puissent être provoquées par l'utilisateur. Par exemple, on demande à l'utilisateur de saisir au clavier un entier et il tape une chaîne de caractères… problème. Nous avons déjà rencontré cette situation : souvenez-vous du programme bissextile.
annee = input() # On demande à l'utilisateur de saisir l'année
annee = int(annee) # On essaye de convertir l'année en un entier
Je vous avais dit que si l'utilisateur fournissait ici une valeur impossible à convertir en entier (une lettre par exemple), le programme plantait. En fait, il lève une exception et Python arrête l'exécution du programme. Si vous testez le programme en faisant un double-clic directement dans l'explorateur, il va se fermer tout de suite (en fait, il affiche bel et bien l'erreur mais se referme aussitôt).
Dans ce cas, et dans d'autres cas similaires, Python permet de tester un extrait de code. S'il ne renvoie aucune erreur, Python continue. Sinon, on peut lui demander d'exécuter une autre action (par exemple, redemander à l'utilisateur de saisir l'année). C'est ce que nous allons voir ici.
On va parler ici de bloc try. Nous allons en effet mettre les instructions que nous souhaitons tester dans un premier bloc et les instructions à exécuter en cas d'erreur dans un autre bloc. Sans plus attendre, voici la syntaxe :
try:
# Bloc à essayer
except:
# Bloc qui sera exécuté en cas d'erreur
Dans l'ordre, nous trouvons :
Le mot-clé try suivi des deux points « : » (try signifie « essayer » en anglais).
Le bloc d'instructions à essayer.
Le mot-clé except suivi, une fois encore, des deux points « : ». Il se trouve au même niveau d'indentation que le try.
Le bloc d'instructions qui sera exécuté si une erreur est trouvée dans le premier bloc.
Reprenons notre test de conversion en enfermant dans un bloc try l'instruction susceptible de lever une exception.
annee = input()
try: # On essaye de convertir l'année en entier
annee = int(annee)
except:
print("Erreur lors de la conversion de l'année.")
Vous pouvez tester ce code en précisant plusieurs valeurs différentes pour la variable annee, comme « 2010 » ou « annee2010 ».
Dans le titre de cette section, j'ai parlé de forme minimale et ce n'est pas pour rien. D'abord, il va de soi que vous ne pouvez intégrer cette solution directement dans votre code. En effet, si l'utilisateur saisit une année impossible à convertir, le système affiche certes une erreur mais finit par planter (puisque l'année, au final, n'a pas été convertie). Une des solutions envisageables est d'attribuer une valeur par défaut à l'année, en cas d'erreur, ou de redemander à l'utilisateur de saisir l'année.
Ensuite et surtout, cette méthode est assez grossière. Elle essaye une instruction et intercepte n'importe quelle exception liée à cette instruction. Ici, c'est acceptable car nous n'avons pas énormément d'erreurs possibles sur cette instruction. Mais c'est une mauvaise habitude à prendre. Voici une manière plus élégante et moins dangereuse.
Nous allons apprendre à compléter notre bloc try. Comme je l'ai indiqué plus haut, la forme minimale est à éviter pour plusieurs raisons.
D'abord, elle ne différencie pas les exceptions qui pourront être levées dans le bloc try. Ensuite, Python peut lever des exceptions qui ne signifient pas nécessairement qu'il y a eu une erreur.
Exécuter le bloc except pour un type d'exception précis
Dans l'exemple que nous avons vu plus haut, on ne pense qu'à un type d'exceptions susceptible d'être levé : le type ValueError, qui trahirait une erreur de conversion. Voyons un autre exemple :
try:
resultat = numerateur / denominateur
except:
print("Une erreur est survenue... laquelle ?")
Ici, plusieurs erreurs sont susceptibles d'intervenir, chacune levant une exception différente.
NameError : l'une des variables numerateur ou denominateur n'a pas été définie (elle n'existe pas). Si vous essayez dans l'interpréteur l'instruction print(numerateur) alors que vous n'avez pas défini la variable numerateur, vous aurez la même erreur.
TypeError : l'une des variables numerateur ou denominateur ne peut diviser ou être divisée (les chaînes de caractères ne peuvent être divisées, ni diviser d'autres types, par exemple). Cette exception est levée car vous utilisez l'opérateur de division « / » sur des types qui ne savent pas quoi en faire.
ZeroDivisionError : encore elle ! Si denominateur vaut 0, cette exception sera levée.
Cette énumération n'est pas une liste exhaustive de toutes les exceptions qui peuvent être levées à l'exécution de ce code. Elle est surtout là pour vous montrer que plusieurs erreurs peuvent se produire sur une instruction (c'est encore plus flagrant sur un bloc constitué de plusieurs instructions) et que la forme minimale intercepte toutes ces erreurs sans les distinguer, ce qui peut être problématique dans certains cas.
Tout se joue sur la ligne du except. Entre ce mot-clé et les deux points, vous pouvez préciser le type de l'exception que vous souhaitez traiter.
try:
resultat = numerateur / denominateur
except NameError:
print("La variable numerateur ou denominateur n'a pas été définie.")
Ce code ne traite que le cas où une exception NameError est levée. On peut intercepter les autres types d'exceptions en créant d'autres blocs except à la suite :
try:
resultat = numerateur / denominateur
except NameError:
print("La variable numerateur ou denominateur n'a pas été définie.")
except TypeError:
print("La variable numerateur ou denominateur possède un type incompatible avec la division.")
except ZeroDivisionError:
print("La variable denominateur est égale à 0.")
C'est mieux non ?
Allez un petit dernier !
On peut capturer l'exception et afficher son message grâce au mot-clé as que vous avez déjà vu dans un autre contexte (si si, rappelez-vous de l'importation de modules).
try:
# Bloc de test
except type_de_l_exception as exception_retournee:
print("Voici l'erreur :", exception_retournee)
Dans ce cas, une variable exception_retournee est créée par Python si une exception du type précisé est levée dans le bloc try.
Je vous conseille de toujours préciser un type d'exceptions après except (sans nécessairement capturer l'exception dans une variable, bien entendu). D'abord, vous ne devez pas utiliser try comme une méthode miracle pour tester n'importe quel bout de code. Il est important que vous gardiez le maximum de contrôle sur votre code. Cela signifie que, si une erreur se produit, vous devez être capable de l'anticiper. En pratique, vous n'irez pas jusqu'à tester si une variable quelconque existe bel et bien, il faut faire un minimum confiance à son code. Mais si vous êtes en face d'une division et que le dénominateur pourrait avoir une valeur de 0, placez la division dans un bloc try et précisez, après le except, le type de l'exception qui risque de se produire (ZeroDivisionError dans cet exemple).
Si vous adoptez la forme minimale (à savoir except sans préciser un type d'exception qui pourrait se produire sur le bloc try), toutes les exceptions seront traitées de la même façon. Et même siexception = erreur la plupart du temps, ce n'est pas toujours le cas. Par exemple, Python lève une exception quand vous voulez fermer votre programme avec le raccourci CTRL + C. Ici vous ne voyez peut-être pas le problème mais si votre bloc try est dans une boucle, vous ne pourrez pas arrêter votre programme avec CTRL + C, puisque l'exception sera traitée par votre except.
Je vous conseille donc de toujours préciser un type d'exception possible après votre except. Vous pouvez bien entendu faire des tests dans l'interpréteur de commandes Python pour reproduire l'exception que vous voulez traiter et ainsi connaître son type.
Les mots-clés else et finally
Ce sont deux mots-clés qui vont nous permettre de construire un bloc try plus complet.
Le mot-clé else
Vous avez déjà vu ce mot-clé et j'espère que vous vous en rappelez. Dans un bloc try, else va permettre d'exécuter une action si aucune erreur ne survient dans le bloc. Voici un petit exemple :
try:
resultat = numerateur / denominateur
except NameError:
print("La variable numerateur ou denominateur n'a pas été définie.")
except TypeError:
print("La variable numerateur ou denominateur possède un type incompatible avec la division.")
except ZeroDivisionError:
print("La variable denominateur est égale à 0.")
else:
print("Le résultat obtenu est", resultat)
Dans les faits, on utilise assez peu else. La plupart des codeurs préfère mettre la ligne contenant leprint directement dans le bloc try. Pour ma part, je trouve que c'est important de distinguer entre le bloc try et ce qui s'effectue ensuite. La ligne du print ne produira vraisemblablement aucune erreur, inutile de la placer dans le bloc try.
Le mot-clé finally
finally permet d'exécuter du code après un bloc try, quelle que soit le résultat de l'exécution dudit bloc. La syntaxe est des plus simples :
try:
# Test d'instruction(s)
except TypeDInstruction:
# Traitement en cas d'erreur
finally:
# Instruction(s) exécutée(s) qu'il y ait eu des erreurs ou non
Est-ce que cela ne revient pas au même si on met du code juste après le bloc ?
Pas tout à fait. Le bloc finally est exécuté dans tous les cas de figures. Quand bien même Python trouverait une instruction return dans votre bloc except par exemple, il exécutera le blocfinally.
Un petit bonus : le mot-clé pass
Il peut arriver, dans certains cas, que l'on souhaite tester un bloc d'instructions… mais ne rien faire en cas d'erreur. Toutefois, un bloc try ne peut être seul.
>>> try:
... 1/0
...
File "<stdin>", line 3
^
SyntaxError: invalid syntax
Il existe un mot-clé que l'on peut utiliser dans ce cas. Son nom est pass et sa syntaxe est très simple d'utilisation :
try:
# Test d'instruction(s)
except type_de_l_exception: # Rien ne doit se passer en cas d'erreur
pass
Je ne vous encourage pas particulièrement à utiliser ce mot-clé mais il existe, et vous le savez à présent.
pass n'est pas un mot-clé propre aux exceptions : on peut également le trouver dans des conditions ou dans des fonctions que l'on souhaite laisser vides.
Voilà, nous avons vu l'essentiel. Il nous reste à faire un petit point sur les assertions et à voir comment lever une exception (ce sera très rapide).
Les assertions sont un moyen simple de s'assurer, avant de continuer, qu'une condition est respectée. En général, on les utilise dans des blocs try … except.
Voyons comment cela fonctionne : nous allons pour l'occasion découvrir un nouveau mot-clé (encore un),assert. Sa syntaxe est la suivante :
assert test
Si le test renvoie True, l'exécution se poursuit normalement. Sinon, une exception AssertionErrorest levée.
Voyons un exemple :
>>> var = 5
>>> assert var == 5
>>> assert var == 8
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>>
Comme vous le voyez, la ligne 2 s'exécute sans problème et ne lève aucune exception. On teste en effet sivar == 5. C'est le cas, le test est donc vrai, aucune exception n'est levée.
À la ligne suivante, cependant, le test est var == 8. Cette fois, le test est faux et une exception du typeAssertionError est levée.
À quoi cela sert-il, concrètement ?
Dans le programme testant si une année est bissextile, on pourrait vouloir s'assurer que l'utilisateur ne saisit pas une année inférieure ou égale à 0 par exemple. Avec les assertions, c'est très facile à faire :
annee = input("Saisissez une année supérieure à 0 :")
try:
annee = int(annee) # Conversion de l'année
assert annee > 0
except ValueError:
print("Vous n'avez pas saisi un nombre.")
except AssertionError:
print("L'année saisie est inférieure ou égale à 0.")
Hmmm… je vois d'ici les mines sceptiques (non non, ne vous cachez pas !). Vous vous demandez probablement pourquoi vous feriez le boulot de Python en levant des exceptions. Après tout, votre travail, c'est en théorie d'éviter que votre programme plante.
Parfois, cependant, il pourra être utile de lever des exceptions. Vous verrez tout l'intérêt du concept quand vous créerez vos propres classes… mais ce n'est pas pour tout de suite. En attendant, je vais vous donner la syntaxe et vous pourrez faire quelques tests, vous verrez de toute façon qu'il n'y a rien de compliqué.
On utilise un nouveau mot-clé pour lever une exception… le mot-clé raise.
raise TypeDeLException("message à afficher")
Prenons un petit exemple, toujours autour de notre programme bissextile. Nous allons lever une exception de type ValueError si l'utilisateur saisit une année négative ou nulle.
annee = input() # L'utilisateur saisit l'année
try:
annee = int(annee) # On tente de convertir l'année
if annee<=0:
raise ValueError("l'année saisie est négative ou nulle")
except ValueError:
print("La valeur saisie est invalide (l'année est peut-être négative).")
Ce que nous venons de faire est réalisable sans l'utilisation des exceptions mais c'était surtout pour vous montrer la syntaxe dans un véritable contexte. Ici, on lève une exception que l'on intercepte immédiatement ou presque, l'intérêt est donc limité. Bien entendu, la plupart du temps ce n'est pas le cas.
Il reste des choses à découvrir sur les exceptions, mais on en a assez fait pour ce chapitre et cette partie. Je ne vous demande pas de connaître toutes les exceptions que Python est amené à utiliser (certaines d'entre elles pourront d'ailleurs n'exister que dans certains modules). En revanche, vous devez être capables de savoir, grâce à l'interpréteur de commandes, quelles exceptions peuvent être levées par Python dans une situation donnée.
En résumé
On peut intercepter les erreurs (ou exceptions) levées par notre code grâce aux blocstryexcept.
La syntaxe d'une assertion est assert test:.
Les assertions lèvent une exception AssertionError si le test échoue.
On peut lever une exception grâce au mot-clé raise suivi du type de l'exception.