Exceptions B : else et finally — la structure complète

Objectifs de la leçon
  • Expliquer quand s’exécute chacun des quatre blocs — try, except, else, finally
  • Utiliser else pour du code qui ne s’exécute que s’il n’y a pas eu d’erreur
  • Utiliser finally pour les actions de nettoyage qui doivent toujours avoir lieu
  • Prédire l’ordre des affichages dans un programme avec un try complet

Qu’allons-nous apprendre ?

Dans la leçon précédente, nous avons utilisé try et except. La structure comporte deux autres parties :

  • else — s’exécute seulement s’il n’y a pas eu d’erreur.
  • finally — s’exécute toujours, avec ou sans erreur.

Pourquoi en avons-nous besoin ?

Imaginez un code qui ouvre une connexion à une base de données (les thèmes backend et bases de données), effectue une action et ferme la connexion. Que se passe-t-il si l’action échoue au milieu ? Si la fermeture est écrite après l’action, elle ne sera pas exécutée, et la connexion reste ouverte. Après une centaine d’échecs de ce genre, le serveur plantera faute de connexions disponibles.

finally garantit que le nettoyage a lieu dans tous les cas. else garde le try petit, et rend clair ce qui ne s’exécute qu’après une réussite.

Explication à partir des bases

Les quatre blocs

BlocQuand il s’exécuteÀ quoi il sert
tryOn commence toujours par luiLe code qui peut échouer
exceptSeulement si une erreur du type correspondant a été lancéeGérer l’erreur
elseSeulement si aucune erreur n’a été lancée dans le tryLa suite qui dépend de la réussite
finallyToujours, à la fin, dans tous les casLe nettoyage : fermer des fichiers, des connexions, un message de fin
Analogie : la cuisine

try = vous essayez de faire un gâteau. except = s’il a brûlé, vous en commandez un à la boulangerie. else = s’il est réussi, vous le décorez (on ne décore pas un gâteau brûlé !). finally = dans tous les cas, vous éteignez le four et faites la vaisselle.

Pourquoi else et pas simplement continuer dans le try ?

On peut mettre le code de la suite à l’intérieur du try. Mais alors, si ce code échoue avec la même erreur, le except l’attrapera par erreur, et traitera le mauvais problème. else rend les choses claires : seulement la ligne risquée dans le try, et le reste à l’extérieur.

finally s’exécute même quand…

  • Il y a eu une erreur et elle a été attrapée.
  • Il y a eu une erreur et elle n’a pas été attrapée. finally s’exécute, puis le programme plante.
  • Il y a un return au milieu du try. finally s’exécute avant que la fonction ne renvoie vraiment.

Syntaxe

try:
    # risky code
except SomeError:
    # runs only if SomeError happened
else:
    # runs only if NO error happened in try
finally:
    # runs ALWAYS, last

Ordre obligatoire : try → except (un ou plusieurs) → else (facultatif) → finally (facultatif). On peut aussi avoir try + finally sans except.

Premier exemple de code

week-2/full_try.py :

def convert(text):
    print("--- converting:", text)
    try:
        number = int(text)
    except ValueError:
        print("except: not a number")
    else:
        print("else: success, doubled =", number * 2)
    finally:
        print("finally: done with", text)

convert("21")
convert("hi")

Sortie :

--- converting: 21
else: success, doubled = 42
finally: done with 21
--- converting: hi
except: not a number
finally: done with hi

Analyse du code ligne par ligne

Blocconvert("21")convert("hi")
try: number = int(text)Réussit : number = 21ValueError
except ValueError:SautéS’exécute
else:S’exécute : affiche 42Sauté
finally:S’exécuteS’exécute

else et except ne s’exécutent jamais tous les deux : soit il y a eu une erreur, soit il n’y en a pas eu. finally, en revanche, s’exécute dans les deux cas.

Un autre exemple

Une « connexion » qui doit être fermée. Ici, nous simulons une connexion à une ressource externe (comme une base de données), et voyons que finally la ferme aussi bien quand quelque chose échoue au milieu que lorsqu’il y a un return. week-2/connection_demo.py :

def open_connection():
    print("[connection opened]")

def close_connection():
    print("[connection closed]")

def fetch_price(product_name, prices):
    open_connection()
    try:
        price = prices[product_name]
    except KeyError:
        print("No such product:", product_name)
        return None
    else:
        print("Found", product_name)
        return price
    finally:
        close_connection()

catalog = {"bread": 8, "milk": 6}

print("Result:", fetch_price("milk", catalog))
print()
print("Result:", fetch_price("cake", catalog))

Sortie :

[connection opened]
Found milk
[connection closed]
Result: 6

[connection opened]
No such product: cake
[connection closed]
Result: None

Analyse :

  • catalog = {"bread": 8, "milk": 6} — c’est un dict (dictionnaire) : chaque clé ("bread") est associée à une valeur (8). prices["milk"] renvoie 6. Nous l’apprendrons en détail dans le thème structures de données et fonctions. Pour l’instant, il suffit de savoir qu’accéder à une clé qui n’existe pas lance une KeyError.
  • Remarquez : il y a un return à la fois dans except et dans else. Et pourtant [connection closed] est affiché avant le Result. Autrement dit, finally s’exécute après que le return a été « décidé », mais avant que la fonction ne renvoie vraiment.
  • Sans finally, il faudrait écrire close_connection() avant chaque return. Facile à oublier, et beaucoup de duplication.

Erreur courante

Erreur 1 : mettre le nettoyage après le try/except au lieu de dans finally.

open_connection()
try:
    price = prices[name]
except KeyError:
    print("missing")
    return None
close_connection()   # never reached when except returns!

En cas de KeyError, le return quitte la fonction, et close_connection() ne s’exécute pas. La connexion reste ouverte. finally résout exactement ce problème.

Erreur 2 : le code de la suite dans le try, et un except qui attrape la mauvaise erreur.

try:
    age = int(text)
    category = ["child", "adult"][age // 18]   # can also raise IndexError... or ValueError?
except ValueError:
    print("Age must be a number")

Si la ligne de la suite échoue, par exemple avec une IndexError pour l’âge 40 (40 // 18 = 2, et il n’y a pas d’indice 2), le message n’a aucun rapport. Ne mettez que int(text) dans le try, et la suite dans else.

Erreur 3 : croire que finally « attrape » l’erreur. Ce n’est pas le cas. S’il n’y a pas de except correspondant, finally s’exécute, et ensuite le programme plante avec un Traceback.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: Prédire la sortie

Écrivez ce qui sera affiché pour chaque appel — avant de l’exécuter :

def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)

half("8")
half("x")
Afficher la solution
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)


half("8")
half("x")
half is 4.0
checked 8
not a number: x
checked x
  • Pour "8", il n’y a pas d’erreur, donc else s’exécute, suivi de finally.
  • Pour "x", une ValueError est lancée, donc except s’exécute — et else ne s’exécute pas. finally s’exécute dans les deux cas.

Exercice 2: finally s’exécute même après return

def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)

print(check(5))
print(check(-2))

Qu’est-ce qui sera affiché, et dans quel ordre ? Expliquez pourquoi.

Afficher la solution
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)


print(check(5))
print(check(-2))
done checking 5
positive
done checking -2
not positive

finally s’exécute avant que la fonction ne renvoie vraiment — même quand il y a un return dans le try. C’est pourquoi « done checking » est affiché avant la valeur renvoyée par la fonction.

Exercice 3: Les quatre blocs

Écrivez une fonction parse_quantity(text) qui utilise les quatre blocs :

  • try — convertit le texte en nombre ;
  • except — affiche invalid: <text> ;
  • else — affiche quantity: <number> ;
  • finally — affiche ---.
parse_quantity("3")
parse_quantity("three")

Résultat attendu :

quantity: 3
---
invalid: three
---
Afficher la solution
def parse_quantity(text):
    try:
        quantity = int(text)
    except ValueError:
        print("invalid:", text)
    else:
        print("quantity:", quantity)
    finally:
        print("---")


parse_quantity("3")
parse_quantity("three")
quantity: 3
---
invalid: three
---

Le code qui ne s’exécute que lorsque tout a réussi est dans else et non dans le try — ainsi le try reste petit, et n’attrape que la ligne qui peut vraiment échouer.

Exercice 4: Fermer ce que l’on a ouvert

Les fonctions open_register() et close_register() ouvrent et ferment une caisse enregistreuse. Écrivez une fonction sell(prices, position) qui ouvre la caisse, renvoie le prix à la position choisie (la position est un indice de la liste — elle commence à 0), et ferme toujours la caisse — même quand la position n’existe pas. Une position qui n’existe pas renvoie None.

def open_register():
    print("register opened")

def close_register():
    print("register closed")

print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))

Résultat attendu :

register opened
register closed
20
register opened
register closed
None
Afficher la solution
def open_register():
    print("register opened")


def close_register():
    print("register closed")


def sell(prices, position):
    open_register()
    try:
        return prices[position]
    except IndexError:
        return None
    finally:
        close_register()


print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
register opened
register closed
20
register opened
register closed
None

C’est exactement le rôle de finally : le nettoyage — fermer une ressource qui a été ouverte, quelle que soit la façon dont la fonction s’est terminée.

Exercice 5: else ou dans le try ?

Deux développeurs ont écrit le même code de deux façons :

# A                              # B
try:                             try:
    number = int(text)               number = int(text)
    print(100 / number)          except ValueError:
except ValueError:                   print("not a number")
    print("not a number")        else:
                                     print(100 / number)

Quelle est la différence, et quelle version est la meilleure ? Que se passera-t-il dans chacune quand text vaut "0" ?

Afficher la solution
  • Dans A, la division est aussi dans le try. Dans B, seule la conversion est dans le try, et la division est passée dans else.
  • Quand text vaut "0", les deux versions lancent une ZeroDivisionError, qu’aucun except n’attrape, et le programme plante. Mais dans B, il est tout à fait clair que le except ValueError ne concerne que la conversion.
  • B est meilleure : un try aussi petit que possible. Ainsi, except n’attrape pas par erreur des erreurs venant d’autres lignes, et il est facile de voir ce que fait chaque partie.

Résumé

  • try — le code risqué. except — seulement si une erreur correspondante s’est produite. else — seulement s’il n’y a pas eu d’erreur. finally — toujours.
  • except et else ne s’exécutent jamais ensemble.
  • finally s’exécute même après un return, et aussi quand l’erreur n’a pas été attrapée (et le programme plante ensuite).
  • else garde le try petit et ciblé. finally est l’endroit du nettoyage : fermer les fichiers et les connexions.

Documentation officielle