{{tag>cours python dev todo}} ====== Python: les exceptions ====== Depuis que nous avons commencé à jouer avec Python, vous les avez sans doute déjà rencontrées, mais vous ne savez sans doute pas vraiment quoi en faire ni ce que c'est. Ce sont **les exceptions**. Dans cette vidéo, nous allons vous expliquer comment gérer les exceptions. Il y a trois choses importantes à savoir sur les exceptions. - C'est que premièrement une exception n'est pas une fatalité, c'est un mécanisme de communication d'erreurs tout à fait normales dans un programme, et on est capable de les capturer et de réagir à une exception. - Deuxièmement, les exceptions fournissent de l'information sur l'erreur qui se produit, c'est donc un mécanisme de notification d'erreur extrêmement utile. - Et troisièmement, les exceptions étant très efficaces en Python, c'est un mécanisme qu'on utilise couramment dans un fonctionnement normal d'un programme. Ouvrons maintenant un éditeur IDLE pour commencer à jouer avec les exceptions. Commençons par écrire une petite fonction toute simple: def div(a, b) C'est une fonction qui prend deux arguments a et b, et qui va juste faire un print de a divisé par b. Absolument rien de compliqué, rien de subtil une simple fonction qui fait une division. Je sauvegarde, je vous rappelle que dans IDLE pour exécuter, je sauvegarde avec Ctrl-S, et j'exécute avec la touche F5. Et je vois à gauche, dans mon interpréteur Python, la fonction qui vient de s'exécuter. Je peux donc maintenant l'appeler, faire un div de 1 par 2 et je vois le résultat : 0.5. Tout à fait normal. Que se passe-t-il maintenant si je fais un div de 1 par 0 ? Donc une division par 0 ? Nous savons que les divisions par 0 sont impossibles ; je vais donc avoir une erreur d'exécution. Regardons ce qui se passe. Je vois en effet une erreur d'exécution et on va prendre quelques instants pour la détailler. La dernière ligne de cette erreur donne le nom de l'exception qui s'appelle *ZeroDivisionError*. Les exceptions finissent en général lorsqu'elles notifient des erreurs par le mot Error. Et j'ai un message d'erreur: division by zero. Ensuite, la ligne juste au dessus, je vois apparaître la ligne de mon code où l'exception est apparue: print de a divisé par b. Et ensuite, la ligne juste au dessus, je vois le fichier dans lequel c'est apparu, donc on voit que c'est à la deuxième ligne de ce fichier, et je vois l'appel qui a produit cette erreur: div(1, 0). On voit donc que l'exception fournit énormément d'informations, elle donne la raison de l'erreur, et elle permet également de situer cette erreur dans le contexte d'exécution de mon programme. Seulement cette exception n'est pas une fatalité, je peux la capturer et on va regarder comment capturer cette exception. Dans ma fonction, je vais rajouter un bloc qu'on appelle*try - except*, qui permet de capturer les exceptions. Tout ce qui est entre le try et le except va être évalué et si j'ai une exception qui est produite dans ce bloc de code, donc dans le bloc de code du try, je vais regarder si cette exception a été capturée par mon code. Comment je capture cette exception ? En écrivant*except*et en donnant un nom d'exception, dans ce cas-là, je veux capturer l'exception*ZeroDivisionError*. Je mets un : et dans le bloc de code du except, je vais écrire ce qui doit s'exécuter lorsque cette exception est produite, et je vais simplement écrire print "attention, division par 0". Voilà, tout simplement. Et ensuite, en sortie de cette instruction*try - except*, je vais juste écrire un print "continuons". Je sauvegarde avec Ctrl-S, j'exécute avec F5 et maintenant, je vais réappeler ma fonction div. Appelons div de 1 par 2. Je vois que j'obtiens mon résultat ; évidemment, "continuons" s'affiche. Ensuite, que se passe-t-il si je fais un div de 1 par 0 ? Je vois que mon exception a été produite, mais elle a été correctement capturée par la clause**except**. En effet, j'ai*except ZeroDivisionError*. Le bloc d'instructions de ma clause except a été exécuté, "attention, division par 0", et mon programme a continué à s'exécuter normalement. On voit donc que l'on peut tout à fait capturer une exception, avoir un comportement approprié qui réagisse à cet exception, et continuer l'exécution de notre programme. Maintenant, regardons ce qu'il se passe si, au lieu de faire une division de 1 par 0, je fais une division de 1 par une chaîne de caractères qui représente le 0. C'est quelque chose qui peut se produire dans un programme où on manipule des entrées et à un moment, on se retrouve avec une chaîne de caractères qui aurait dû être convertie en entier mais qui n'a pas été convertie en entier. Exécutons ce code et regardons l'erreur. J'ai une erreur qui s'appelle*TypeError*, et qui me dit encore une fois de manière très explicite, c'est affiché dans la dernière ligne de mon exception, que l'opération division n'est pas supportée entre les entiers et les chaînes de caractères. De nouveau, je peux tout à fait réagir à cette exception en rajoutant une**clause except**. Je peux donc ajouter autant de clauses except que je veux pour réagir à des exceptions particulières. Là, je vais réagir à*TypeError*et je vais afficher un petit message me permettant d'expliquer exactement quel est le problème, je vais juste écrire "il faut des int". Voilà, donc un message tout simple. J'exécute ce morceau de code, et je vais réappeler ma fonction div de 1 par 2, je vois qu'il s'affiche: 0.5, "continuons" ; div de 1 par 0, je vois qu'il s'affiche: "attention, division par 0", "continuons" ; div de 1 par la chaîne de caractères '0', je vois qu'il s'affiche: "il faut des int", "continuons". Je vois donc que mon mécanisme d'exception en Python me permet de capturer n'importe quelle exception et d'avoir un comportement approprié. En Python, vous pouvez avoir une clause except sans spécifier aucune exception. C'est en général une très mauvaise pratique. Pourquoi ? Parce que ça va cacher les exceptions qui sont produites par votre code. Prenez le cas que l'on vient de regarder. Supposons que vous vous disiez: de toute façon, quelle est l'erreur qui peut se produire avec une division ? Essentiellement, ce n'est qu'une division par zéro. Et que vous fassiez simplement, je vais vous montrer ça, juste un except qui affiche simplement "attention, division par 0". J'exécute ce code, et maintenant je fais une division de 1 par 0, on se dit: oui, je l'ai testé, ça fonctionne tout à fait ; mais si maintenant je fais un div de 1 par la chaîne de caractères '0', je vais avoir un message d'erreur qui n'est pas approprié, mon exception a été cachée, je ne connais pas la raison du problème dans mon programme et mon programme risque de planter plus loin dans le code. D'une manière générale,**on doit toujours capturer les exceptions**que l'on a étudiées et que l'on sait qu'elles vont se produire dans notre code, et laisser remonter les exceptions que nous n'avons pas prévues. Une caractéristique importante des exceptions, c'est qu'elles*bubblent*. Ça veut dire qu'elles vont remonter votre pile d'exécution jusqu'à arrêter le programme. Regardons l'illustration de ce mécanisme de*bubbling*. Je vais définir une fonction*div*, que j'ai déjà définie, qui prend juste deux arguments a et b, et qui va juste faire un print de a divisé par b. Et ensuite, je vais définir une fonction f qui prend un seul argument x et qui va appeler div de 1 par x. On voit que j'ai deux fonctions, une fonction div et une fonction f, et que ma fonction div est appelée par une autre fonction, la fonction f. Exécutons cela. Regardons ce qu'il va se passer. Lorsque j'appelle ma fonction f avec par exemple 1, ma fonction va exécuter la fonction div mais tant que ma fonction div n'a pas retourné de valeur la fonction f reste en cours d'exécution. On dit qu'elle reste dans la pile d'exécution. En fait, la pile d'exécution va contenir toutes les fonctions de notre programme qui ont été appelées mais qui n'ont pas encore retourné de valeur. Si je fais f(1), j'appelle div(1,1). Je vais exécuter et je vais voir afficher 1. Si maintenant je fais f(0), que va-t-il se passer ? Je vais appeler la fonction div(1, 0) je vais faire un print de 1 divisé par 0, je vais avoir une exception, mon exception va être produite par la division a divisé par b à l'intérieur du print. L'exception va être produite, elle va arrêter l'exécution de ma fonction mais ma fonction a été appelée par f, l'exception va donc remonter la pile d'exécution, elle va sortir dans ma fonction f, elle n'a pas été capturée, elle va remonter la pile d'exécution et elle va arrêter mon programme. Regardons l'exécution de ça. Ce mécanisme de**bubbling** a deux avantages majeurs. Le premier avantage, c'est que je peux capturer mon exception n'importe où le long de la pile d'exécution, je peux tout à fait capturer mon exception au niveau du print, je peux la capturer au niveau de la fonction f, dans ma fonction f, au niveau de l'appel de div, et je peux tout à fait la capturer lors de l'appel de f. Le deuxième avantage de cette remontée de l'exception, c'est que ma trace d'exécution va être capable de me dire exactement par où est passée mon exception et par conséquent, va me fournir des informations précieuses sur le diagnostic du problème dans mon programme. Regardons cela. Je vois que j'ai une exception qui s'appelle ZeroDivisionError qui a été produite par print de a divisé par b. Si je remonte, je vois que cette exception a eu lieu dans la fonction div, et que cette fonction div a été appelée par l'appel de f(0). J'ai donc une trace exacte de la remontée de mon exception qui me permet de diagnostiquer l'origine du problème, dans ce cas-là, l'appel de f(0). Nous venons de voir le mécanisme des exceptions en Python. Une bonne pratique en Python est de capturer les exceptions que vous connaissez et de les capturer au plus près de l'endroit où elles se produisent dans votre code. En effet, plus vous les capturez tôt, et plus votre réaction peut être appropriée à l'origine du problème. Une question que vous pouvez vous poser, c'est comment est-ce que je fais pour connaître les exceptions que je dois capturer ? Bien là, il n'y a pas de miracle, la seule réponse à cette question est qu'il faut lire en détail la documentation des modules que vous utilisez ou des objets que vous utilisez.