Usage#

Pile d’appel ou call stack#

La pile d’appel (ou pile d’exécution ou call stack) mémorise les appels de fonctions. La premièrel ligne est le point d’entrée du programme. La suivante est la seconde fonction appelée. Si celle-ci en appelle une autre, une autre ligne est ajoutée et celle-ci demeure jusqu’à ce qu’elle est terminée son exécution. A chaque instant, la dernière ligne est la fonction en train de s’exécuter, les lignes précédentes définissent le chemin que l’ordinateur a suivi pour arriver jusque là.

<<<

import traceback
import sys


def foncA():
    print("foncA begin")
    foncB()
    print("foncA end")


def foncB():
    print("foncB begin")
    foncC()
    print("foncB end")


def foncC():
    print("foncC begin")
    try:
        raise Exception("erreur volontaire")
    except Exception:
        print("Erreur")
        print("\n".join(traceback.format_stack()))
    print("foncC end")


foncA()

>>>

    foncA begin
    foncB begin
    foncC begin
    Erreur
      File "/usr/lib/python3.10/runpy.py", line 196, in _run_module_as_main
        return _run_code(code, main_globals, None,
    
      File "/usr/lib/python3.10/runpy.py", line 86, in _run_code
        exec(code, run_globals)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/__main__.py", line 5, in <module>
        raise SystemExit(main())
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/cmd/build.py", line 326, in main
        return build_main(argv)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/cmd/build.py", line 290, in build_main
        app.build(args.force_all, args.filenames)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/application.py", line 351, in build
        self.builder.build_update()
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/builders/__init__.py", line 290, in build_update
        self.build(to_build,
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/builders/__init__.py", line 310, in build
        updated_docnames = set(self.read())
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/builders/__init__.py", line 417, in read
        self._read_serial(docnames)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/builders/__init__.py", line 438, in _read_serial
        self.read_doc(docname)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/builders/__init__.py", line 494, in read_doc
        publisher.publish()
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/core.py", line 234, in publish
        self.document = self.reader.read(self.source, self.parser,
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/io.py", line 104, in read
        self.parse()
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/readers/__init__.py", line 76, in parse
        self.parser.parse(self.input, document)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/sphinx/parsers.py", line 80, in parse
        self.statemachine.run(inputlines, document, inliner=self.inliner)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 169, in run
        results = StateMachineWS.run(self, input_lines, input_offset,
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/statemachine.py", line 233, in run
        context, next_state, result = self.check_line(
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/statemachine.py", line 445, in check_line
        return method(match, context, next_state)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 3024, in text
        self.section(title.lstrip(), source, style, lineno + 1, messages)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 325, in section
        self.new_subsection(title, lineno, messages)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 391, in new_subsection
        newabsoffset = self.nested_parse(
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 279, in nested_parse
        state_machine.run(block, input_offset, memo=self.memo,
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 195, in run
        results = StateMachineWS.run(self, input_lines, input_offset)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/statemachine.py", line 233, in run
        context, next_state, result = self.check_line(
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/statemachine.py", line 445, in check_line
        return method(match, context, next_state)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 2785, in underline
        self.section(title, source, style, lineno - 1, messages)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 325, in section
        self.new_subsection(title, lineno, messages)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 391, in new_subsection
        newabsoffset = self.nested_parse(
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 279, in nested_parse
        state_machine.run(block, input_offset, memo=self.memo,
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 195, in run
        results = StateMachineWS.run(self, input_lines, input_offset)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/statemachine.py", line 233, in run
        context, next_state, result = self.check_line(
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/statemachine.py", line 445, in check_line
        return method(match, context, next_state)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 2355, in explicit_markup
        nodelist, blank_finish = self.explicit_construct(match)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 2367, in explicit_construct
        return method(self, expmatch)
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 2104, in directive
        return self.run_directive(
    
      File "/home/xadupre/.local/lib/python3.10/site-packages/docutils/parsers/rst/states.py", line 2154, in run_directive
        result = directive_instance.run()
    
      File "/home/xadupre/github/sphinx-runpython/sphinx_runpython/runpython/sphinx_runpython_extension.py", line 789, in run
        out, err, context = run_python_script(
    
      File "/home/xadupre/github/sphinx-runpython/sphinx_runpython/runpython/sphinx_runpython_extension.py", line 431, in run_python_script
        exec(obj, globs, loc)
    
      File "", line 26, in <module>
    
      File "", line 25, in run_python_script_139790764159936
    
      File "", line 8, in foncA
    
      File "", line 13, in foncB
    
      File "", line 22, in foncC
    
    foncC end
    foncB end
    foncA end

Récupération de la pile d’appel#

Le module traceback permet de récupérer la pile d’appels lorsqu’une exception survient.

<<<

def raise_exception():
    raise Exception("an error was raised")


try:
    insidefe()
except:
    exc_type, exc_value, exc_traceback = sys.exc_info()
    print("".join(traceback.format_tb(exc_traceback)))

>>>

      File "", line 7, in run_python_script_139790762644608

Il est possible de récupérer la liste des appels de fonctions avec la fonction extract_tb. Cette information est précieuse pour écrire un test qui vérifie qu’une erreur s’est bien produite à un endroit particulier, de détecter les cas particuliers comme les boucles infinies ou d’améliorer un message d’erreur en cas de besoin (lire How do I write Flask’s excellent debug log message to a file in production?).

Message d’erreur plus explicite#

Lorsqu’une erreur se produit dans une librairie de Python, le message ne mentionne aucune information à propos du code qui l’a provoquée.

<<<

import math

ensemble = [1, 0, 2]
s = 0
for e in ensemble:
    s += math.log(e)

>>>

    
    [runpythonerror]
    
    Traceback (most recent call last):
        exec(obj, globs, loc)
      File "", line 8, in <module>
      File "", line 7, in run_python_script_139790761486464
    ValueError: math domain error

Typiquement dans ce cas précis, on ne sait pas quel est l’indice de l’élément qui a provoqué l’erreur. On utilise alors un mécanisme qui permet d’ajouter une erreur sans perdre les informations l’exception originale. Ce mécanisme est souvent utilisé pour donner plus d’information à l’utilisateur de la fonction, plus que le message d’erreur initial.

<<<

import math

ensemble = [1, 0, 2]
s = 0
for i, e in enumerate(ensemble):
    try:
        s += math.log(e)
    except Exception as exc:
        raise Exception(f"Issue with element {i} and log function.") from exc

>>>

    
    [runpythonerror]
    
    Traceback (most recent call last):
      File "", line 8, in run_python_script_139790765229312
    ValueError: math domain error
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
        exec(obj, globs, loc)
      File "", line 11, in <module>
      File "", line 10, in run_python_script_139790765229312
    Exception: Issue with element 1 and log function.

La dernière partie de la dernière ligne est importante : from exc. Le langage garde ainsi la trace de la première exception.

Type d’exception#

Le langage Python propose des types d’exceptions prédéfinis : Built-in Exceptions. Chaque type correspond à un type d’erreur particulier. Il est toujours préférable d’attraper un type d’exception précis plutôt que le type générique Exception. De cette façon, tout autre type d’exception sera toujours considéré comme une erreur.

<<<

import math

ensemble = [1, 0, 2]
s = 0
for i, e in enumerate(ensemble):
    try:
        s += math.log(e)
    except ValueError as exc:
        raise ValueError(f"Issue with element {i} and log function.") from exc

>>>

    
    [runpythonerror]
    
    Traceback (most recent call last):
      File "", line 8, in run_python_script_139790765136896
    ValueError: math domain error
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
        exec(obj, globs, loc)
      File "", line 11, in <module>
      File "", line 10, in run_python_script_139790765136896
    ValueError: Issue with element 1 and log function.

De la même manière, il est préférable en d’erreur de lancer une exception d’un type précis. Plus le type est restrictif, plus l’information retournée au développeur utilisant la fonction est précise et celui-ci peut choisir d’intercepter un type précis d’exception.

Conventions#

Si le programme ne peut continuer, il est d’usage de lancer une exception avec un message d’erreur suffisamment explicite pour dire à celui qui utilise le programme comment faire pour corriger le problème. Pour ce faire, le message est important, le type d’exception aussi.

En règle générale, le programme ne continue pas après avoir lancé une exception. Mais comme Python dispose d’un garbage collector, l’interpréteur se charge lui-même de détruire ce qui n’est plus nécessaire si l’utilisateur du code fautif intercepte l’exception et continue l’exécution du programme.

try:
    with open("file_to_read.txt", "r") as f:
        content = f.read()
except FileNotFoundError:
    # On continue malgré tout et on récupère les données
    # souhaitées autrement.
    content = download_content()

Toutefois il est préférable d’écrire ce qui suit car d’autres langages de programmation sont moins permissifs.

if os.path.exists("file_to_read.txt"):
    with open("file_to_read.txt", "r") as f:
        content = f.read()
else:
    # On continue malgré tout et on récupère les données
    # souhaitées autrement.
    content = download_content()

Attraper une exception est parfois nécessaire si celle-ci se produit dans une fonction dont le code n’est pas modifiable.

try:
    content = read_fromf_file("file_to_read.txt")
except FileNotFoundError:
    # On continue malgré tout et on récupère les données
    # souhaitées autrement.
    content = download_content()

Dans ce cas, même si le langage python détruit la plupart des variables qui ne sont plus utilisées. Il n’en est pas toujours de même avec des ressources comme un fichier, un accès internet…

try:
    f = open(name, "r")
    content = f.read()  # l'erreur se produit ici
    f.close()
except UnicodeEncodeError as e:
    # Le fichier contient un caractère inattendu.
    raise ValueError(f"Unable to read file {name!r}.") from e

Dans l’exemple précédent, le fichier f n’est pas jamais fermé. L’utilisateur ne pourra pas le supprimer ou le réécrire jusqu’à ce ce que f.close() soit exécuté ou l’interpréteur python terminé.

<<<

name = "essai.txt"
with open(name, "w", encoding="utf-8") as f:
    f.write("ééééééé")

try:
    f = open(name, "r", encoding="ascii")
    content = f.read()  # l'erreur se produit ici
    f.close()
except UnicodeEncodeError as e:
    # Le fichier contient un caractère inattendu.
    print(f"unable to read file {name!r} ({e})")

with open(name, "w", encoding="utf-8") as f:
    f.write("àààààààà")

>>>

    
    [runpythonerror]
    
    Traceback (most recent call last):
        exec(obj, globs, loc)
      File "", line 17, in <module>
      File "", line 9, in run_python_script_139790764137216
      File "/usr/lib/python3.10/encodings/ascii.py", line 26, in decode
        return codecs.ascii_decode(input, self.errors)[0]
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)