Recherche de site Web

Comprendre le traçage en Python


Lorsqu'une exception se produit dans un programme Python, un traçage est souvent imprimé. Savoir comment lire le traçage peut vous aider à identifier facilement l'erreur et à la corriger. Dans ce tutoriel, nous allons voir ce que le traçage peut vous dire.

Après avoir terminé ce tutoriel, vous saurez :

  • Comment lire un traçage
  • Comment imprimer la pile d'appels sans exception
  • Ce qui n'est pas affiché dans le traçage

Démarrez votre projet avec mon nouveau livre Python pour l'apprentissage automatique, comprenant des tutoriels pas à pas et les fichiers code source Python pour tous exemples.

Présentation du didacticiel

Ce didacticiel est divisé en quatre parties ; ils sont:

  1. La hiérarchie des appels d'un programme simple
  2. Retraçage en cas d'exception
  3. Déclencher le traçage manuellement
  4. Un exemple dans la formation de modèles

La hiérarchie des appels d'un programme simple

Prenons un programme simple :

def indentprint(x, indent=0, prefix="", suffix=""):
    if isinstance(x, dict):
        printdict(x, indent, prefix, suffix)
    elif isinstance(x, list):
        printlist(x, indent, prefix, suffix)
    elif isinstance(x, str):
        printstring(x, indent, prefix, suffix)
    else:
        printnumber(x, indent, prefix, suffix)

def printdict(x, indent, prefix, suffix):
    spaces = " " * indent
    print(spaces + prefix + "{")
    for n, key in enumerate(x):
        comma = "," if n!=len(x)-1 else ""
        indentprint(x[key], indent+2, str(key)+": ", comma)
    print(spaces + "}" + suffix)

def printlist(x, indent, prefix, suffix):
    spaces = " " * indent
    print(spaces + prefix + "[")
    for n, item in enumerate(x):
        comma = "," if n!=len(x)-1 else ""
        indentprint(item, indent+2, "", comma)
    print(spaces + "]" + suffix)

def printstring(x, indent, prefix, suffix):
    spaces = " " * indent
    print(spaces + prefix + '"' + str(x) + '"' + suffix)

def printnumber(x, indent, prefix, suffix):
    spaces = " " * indent
    print(spaces + prefix + str(x) + suffix)

data = {
    "a": [{
        "p": 3, "q": 4,
        "r": [3,4,5],
    },{
        "f": "foo", "g": 2.71
    },{
        "u": None, "v": "bar"
    }],
    "c": {
        "s": ["fizz", 2, 1.1],
        "t": []
    },
}

indentprint(data)

Ce programme imprimera le dictionnaire Python data avec des indentations. Sa sortie est la suivante :

{
  a: [
    {
      p: 3,
      q: 4,
      r: [
        3,
        4,
        5
      ]
    },
    {
      f: "foo",
      g: 2.71
    },
    {
      u: None,
      v: "bar"
    }
  ],
  c: {
    s: [
      "fizz",
      2,
      1.1
    ],
    t: [
    ]
  }
}

C'est un programme court, mais les fonctions s'appellent entre elles. Si nous ajoutons une ligne au début de chaque fonction, nous pouvons révéler comment la sortie est produite avec le flux de contrôle :

def indentprint(x, indent=0, prefix="", suffix=""):
    print(f'indentprint(x, {indent}, "{prefix}", "{suffix}")')
    if isinstance(x, dict):
        printdict(x, indent, prefix, suffix)
    elif isinstance(x, list):
        printlist(x, indent, prefix, suffix)
    elif isinstance(x, str):
        printstring(x, indent, prefix, suffix)
    else:
        printnumber(x, indent, prefix, suffix)

def printdict(x, indent, prefix, suffix):
    print(f'printdict(x, {indent}, "{prefix}", "{suffix}")')
    spaces = " " * indent
    print(spaces + prefix + "{")
    for n, key in enumerate(x):
        comma = "," if n!=len(x)-1 else ""
        indentprint(x[key], indent+2, str(key)+": ", comma)
    print(spaces + "}" + suffix)

def printlist(x, indent, prefix, suffix):
    print(f'printlist(x, {indent}, "{prefix}", "{suffix}")')
    spaces = " " * indent
    print(spaces + prefix + "[")
    for n, item in enumerate(x):
        comma = "," if n!=len(x)-1 else ""
        indentprint(item, indent+2, "", comma)
    print(spaces + "]" + suffix)

def printstring(x, indent, prefix, suffix):
    print(f'printstring(x, {indent}, "{prefix}", "{suffix}")')
    spaces = " " * indent
    print(spaces + prefix + '"' + str(x) + '"' + suffix)

def printnumber(x, indent, prefix, suffix):
    print(f'printnumber(x, {indent}, "{prefix}", "{suffix}")')
    spaces = " " * indent
    print(spaces + prefix + str(x) + suffix)

et la sortie sera modifiée avec plus d'informations :

indentprint(x, 0, "", "")
printdict(x, 0, "", "")
{
indentprint(x, 2, "a: ", ",")
printlist(x, 2, "a: ", ",")
  a: [
indentprint(x, 4, "", ",")
printdict(x, 4, "", ",")
    {
indentprint(x, 6, "p: ", ",")
printnumber(x, 6, "p: ", ",")
      p: 3,
indentprint(x, 6, "q: ", ",")
printnumber(x, 6, "q: ", ",")
      q: 4,
indentprint(x, 6, "r: ", "")
printlist(x, 6, "r: ", "")
      r: [
indentprint(x, 8, "", ",")
printnumber(x, 8, "", ",")
        3,
indentprint(x, 8, "", ",")
printnumber(x, 8, "", ",")
        4,
indentprint(x, 8, "", "")
printnumber(x, 8, "", "")
        5
      ]
    },
indentprint(x, 4, "", ",")
printdict(x, 4, "", ",")
    {
indentprint(x, 6, "f: ", ",")
printstring(x, 6, "f: ", ",")
      f: "foo",
indentprint(x, 6, "g: ", "")
printnumber(x, 6, "g: ", "")
      g: 2.71
    },
indentprint(x, 4, "", "")
printdict(x, 4, "", "")
    {
indentprint(x, 6, "u: ", ",")
printnumber(x, 6, "u: ", ",")
      u: None,
indentprint(x, 6, "v: ", "")
printstring(x, 6, "v: ", "")
      v: "bar"
    }
  ],
indentprint(x, 2, "c: ", "")
printdict(x, 2, "c: ", "")
  c: {
indentprint(x, 4, "s: ", ",")
printlist(x, 4, "s: ", ",")
    s: [
indentprint(x, 6, "", ",")
printstring(x, 6, "", ",")
      "fizz",
indentprint(x, 6, "", ",")
printnumber(x, 6, "", ",")
      2,
indentprint(x, 6, "", "")
printnumber(x, 6, "", "")
      1.1
    ],
indentprint(x, 4, "t: ", "")
printlist(x, 4, "t: ", "")
    t: [
    ]
  }
}

Nous connaissons maintenant l’ordre dans lequel chaque fonction est invoquée. C'est l'idée d'une pile d'appels. À tout moment, lorsque nous exécutons une ligne de code dans une fonction, nous voulons savoir ce qui a invoqué cette fonction.

Retraçage en cas d'exception

Si nous faisons une faute de frappe dans le code comme suit :

def printdict(x, indent, prefix, suffix):
    spaces = " " * indent
    print(spaces + prefix + "{")
    for n, key in enumerate(x):
        comma = "," if n!=len(x)-1 else ""
        indentprint(x[key], indent+2, str(key)+": ", comma)
    print(spaces + "}") + suffix

La faute de frappe se trouve sur la dernière ligne, où le crochet fermant doit être à la fin de la ligne, pas avant un +. La valeur de retour de la fonction print() est un objet Python None. Et ajouter quelque chose à Aucun déclenchera une exception.

Si vous exécutez ce programme à l'aide de l'interpréteur Python, vous verrez ceci :

{
  a: [
    {
      p: 3,
      q: 4,
      r: [
        3,
        4,
        5
      ]
    }
Traceback (most recent call last):
  File "tb.py", line 52, in 
    indentprint(data)
  File "tb.py", line 3, in indentprint
    printdict(x, indent, prefix, suffix)
  File "tb.py", line 16, in printdict
    indentprint(x[key], indent+2, str(key)+": ", comma)
  File "tb.py", line 5, in indentprint
    printlist(x, indent, prefix, suffix)
  File "tb.py", line 24, in printlist
    indentprint(item, indent+2, "", comma)
  File "tb.py", line 3, in indentprint
    printdict(x, indent, prefix, suffix)
  File "tb.py", line 17, in printdict
    print(spaces + "}") + suffix
TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'

Les lignes commençant par « Traceback (dernier appel le plus récent) : » constituent le traçage. Il s'agit de la pile de votre programme au moment où votre programme a rencontré l'exception. Dans l’exemple ci-dessus, le traçage s’effectue dans l’ordre « dernier appel le plus récent ». Par conséquent, votre fonction principale est en haut tandis que celle déclenchant l'exception est en bas. Nous savons donc que le problème se situe à l'intérieur de la fonction printdict().

Habituellement, vous verrez le message d’erreur à la fin du traçage. Dans cet exemple, il s'agit d'une TypeError déclenchée par l'ajout de None et d'une chaîne. Mais l’aide du traçage s’arrête là. Vous devez déterminer lequel est Aucun et lequel est une chaîne. En lisant le traçage, nous savons également que la fonction déclenchant une exception printdict() est invoquée par indentprint(), et elle est à son tour invoquée par printlist() , et ainsi de suite.

Si vous exécutez ceci dans un notebook Jupyter, voici le résultat :

{
  a: [
    {
      p: 3,
      q: 4,
      r: [
        3,
        4,
        5
      ]
    }
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/6z/w0ltb1ss08l593y5xt9jyl1w0000gn/T/ipykernel_37031/2508041071.py in 
----> 1 indentprint(x)

/var/folders/6z/w0ltb1ss08l593y5xt9jyl1w0000gn/T/ipykernel_37031/2327707064.py in indentprint(x, indent, prefix, suffix)
      1 def indentprint(x, indent=0, prefix="", suffix=""):
      2     if isinstance(x, dict):
----> 3         printdict(x, indent, prefix, suffix)
      4     elif isinstance(x, list):
      5         printlist(x, indent, prefix, suffix)

/var/folders/6z/w0ltb1ss08l593y5xt9jyl1w0000gn/T/ipykernel_37031/2327707064.py in printdict(x, indent, prefix, suffix)
     14     for n, key in enumerate(x):
     15         comma = "," if n!=len(x)-1 else ""
---> 16         indentprint(x[key], indent+2, str(key)+": ", comma)
     17     print(spaces + "}") + suffix
     18 

/var/folders/6z/w0ltb1ss08l593y5xt9jyl1w0000gn/T/ipykernel_37031/2327707064.py in indentprint(x, indent, prefix, suffix)
      3         printdict(x, indent, prefix, suffix)
      4     elif isinstance(x, list):
----> 5         printlist(x, indent, prefix, suffix)
      6     elif isinstance(x, str):
      7         printstring(x, indent, prefix, suffix)

/var/folders/6z/w0ltb1ss08l593y5xt9jyl1w0000gn/T/ipykernel_37031/2327707064.py in printlist(x, indent, prefix, suffix)
     22     for n, item in enumerate(x):
     23         comma = "," if n!=len(x)-1 else ""
---> 24         indentprint(item, indent+2, "", comma)
     25     print(spaces + "]" + suffix)
     26 

/var/folders/6z/w0ltb1ss08l593y5xt9jyl1w0000gn/T/ipykernel_37031/2327707064.py in indentprint(x, indent, prefix, suffix)
      1 def indentprint(x, indent=0, prefix="", suffix=""):
      2     if isinstance(x, dict):
----> 3         printdict(x, indent, prefix, suffix)
      4     elif isinstance(x, list):
      5         printlist(x, indent, prefix, suffix)

/var/folders/6z/w0ltb1ss08l593y5xt9jyl1w0000gn/T/ipykernel_37031/2327707064.py in printdict(x, indent, prefix, suffix)
     15         comma = "," if n!=len(x)-1 else ""
     16         indentprint(x[key], indent+2, str(key)+": ", comma)
---> 17     print(spaces + "}") + suffix
     18 
     19 def printlist(x, indent, prefix, suffix):

TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'

Les informations sont essentiellement les mêmes, mais elles vous donnent les lignes avant et après chaque appel de fonction.

Déclencher le traçage manuellement

Le moyen le plus simple d'imprimer un traçage consiste à ajouter une instruction raise pour créer manuellement une exception. Mais cela mettra également fin à votre programme. Si nous voulons imprimer la pile à tout moment, même sans aucune exception, nous pouvons le faire avec ce qui suit :

import traceback

def printdict(x, indent, prefix, suffix):
    spaces = " " * indent
    print(spaces + prefix + "{")
    for n, key in enumerate(x):
        comma = "," if n!=len(x)-1 else ""
        indentprint(x[key], indent+2, str(key)+": ", comma)
    traceback.print_stack()    # print the current call stack
    print(spaces + "}" + suffix)

La ligne traceback.print_stack() imprimera la pile d'appels actuelle.

Mais en effet, nous souhaitons souvent imprimer la pile uniquement lorsqu'il y a une erreur (on en apprend donc davantage sur pourquoi). Le cas d'utilisation le plus courant est le suivant :

import traceback
import random

def compute():
    n = random.randint(0, 10)
    m = random.randint(0, 10)
    return n/m

def compute_many(n_times):
    try:
        for _ in range(n_times):
            x = compute()
        print(f"Completed {n_times} times")
    except:
        print("Something wrong")
        traceback.print_exc()

compute_many(100)

Il s'agit d'un modèle typique de calcul répété d'une fonction, tel que la simulation de Monte Carlo. Mais si nous ne faisons pas assez attention, nous pouvons rencontrer des erreurs, comme dans l’exemple ci-dessus où nous pouvons avoir une division par zéro. Le problème est que dans le cas de calculs plus compliqués, vous ne pouvez pas facilement repérer le défaut. Comme dans ce qui précède, le problème est enfoui dans l'appel à compute(). Par conséquent, il est utile de comprendre comment nous obtenons l’erreur. Mais en même temps, nous souhaitons gérer le cas de l’erreur plutôt que de laisser le programme entier se terminer. Si nous utilisons la construction try-catch, le traceback ne sera pas imprimé par défaut. Par conséquent, nous devons utiliser l'instruction traceback.print_exc() pour le faire manuellement.

En fait, nous pouvons avoir un traçage plus élaboré. Étant donné que le traçage est la pile d'appels, nous pouvons examiner chaque fonction de la pile d'appels et vérifier les variables de chaque niveau. Dans ce cas compliqué, c'est la fonction que j'utilise habituellement pour faire une trace plus détaillée :

def print_tb_with_local():
    """Print stack trace with local variables. This does not need to be in
    exception. Print is using the system's print() function to stderr.
    """
    import traceback, sys
    tb = sys.exc_info()[2]
    stack = []
    while tb:
        stack.append(tb.tb_frame)
        tb = tb.tb_next()
    traceback.print_exc()
    print("Locals by frame, most recent call first", file=sys.stderr)
    for frame in stack:
        print("Frame {0} in {1} at line {2}".format(
            frame.f_code.co_name,
            frame.f_code.co_filename,
            frame.f_lineno), file=sys.stderr)
        for key, value in frame.f_locals.items():
            print("\t%20s = " % key, file=sys.stderr)
            try:
                if '__repr__' in dir(value):
                    print(value.__repr__(), file=sys.stderr)
                elif '__str__' in dir(value):
                    print(value.__str__(), file=sys.stderr)
                else:
                    print(value, file=sys.stderr)
            except:
                print("", file=sys.stderr)

Un exemple de formation sur modèle

La pile d'appels, comme indiqué dans le traçage, présente une limitation : vous ne pouvez voir que les fonctions Python. Cela devrait convenir au programme que vous avez écrit, mais de nombreuses grandes bibliothèques en Python en ont une partie écrite dans un autre langage et compilée en binaire. Un exemple est Tensorflow. Toutes les opérations sous-jacentes sont en binaire pour la performance. Par conséquent, si vous exécutez le code suivant, vous verrez quelque chose de différent :

import numpy as np

sequence = np.arange(0.1, 1.0, 0.1)  # 0.1 to 0.9
n_in = len(sequence)
sequence = sequence.reshape((1, n_in, 1))

# define model
import tensorflow as tf
from tensorflow.keras.layers import LSTM, RepeatVector, Dense, TimeDistributed, Input
from tensorflow.keras import Sequential, Model

model = Sequential([
    LSTM(100, activation="relu", input_shape=(n_in+1, 1)),
    RepeatVector(n_in),
    LSTM(100, activation="relu", return_sequences=True),
    TimeDistributed(Dense(1))
])
model.compile(optimizer="adam", loss="mse")

model.fit(sequence, sequence, epochs=300, verbose=0)

Le paramètre input_shape de la première couche LSTM du modèle doit être (n_in, 1) pour correspondre aux données d'entrée, plutôt que (n_in+1, 1) . Ce code affichera l'erreur suivante une fois que vous aurez invoqué la dernière ligne :

Traceback (most recent call last):
  File "trback3.py", line 20, in 
    model.fit(sequence, sequence, epochs=300, verbose=0)
  File "/usr/local/lib/python3.9/site-packages/keras/utils/traceback_utils.py", line 67, in error_handler
    raise e.with_traceback(filtered_tb) from None
  File "/usr/local/lib/python3.9/site-packages/tensorflow/python/framework/func_graph.py", line 1129, in autograph_handler
    raise e.ag_error_metadata.to_exception(e)
ValueError: in user code:

    File "/usr/local/lib/python3.9/site-packages/keras/engine/training.py", line 878, in train_function  *
        return step_function(self, iterator)
    File "/usr/local/lib/python3.9/site-packages/keras/engine/training.py", line 867, in step_function  **
        outputs = model.distribute_strategy.run(run_step, args=(data,))
    File "/usr/local/lib/python3.9/site-packages/keras/engine/training.py", line 860, in run_step  **
        outputs = model.train_step(data)
    File "/usr/local/lib/python3.9/site-packages/keras/engine/training.py", line 808, in train_step
        y_pred = self(x, training=True)
    File "/usr/local/lib/python3.9/site-packages/keras/utils/traceback_utils.py", line 67, in error_handler
        raise e.with_traceback(filtered_tb) from None
    File "/usr/local/lib/python3.9/site-packages/keras/engine/input_spec.py", line 263, in assert_input_compatibility
        raise ValueError(f'Input {input_index} of layer "{layer_name}" is '

    ValueError: Input 0 of layer "sequential" is incompatible with the layer: expected shape=(None, 10, 1), found shape=(None, 9, 1)

Si vous regardez le traçage, vous ne pouvez pas vraiment voir la pile complète des appels. Par exemple, le cadre supérieur que vous savez a appelé model.fit(), mais le deuxième cadre provient d'une fonction nommée error_handler(). Ici, vous ne pouvez pas voir comment la fonction fit() a déclenché cela. En effet, Tensorflow est hautement optimisé. Beaucoup de choses sont cachées dans le code compilé et ne sont pas visibles par l'interpréteur Python.

Dans ce cas, il est essentiel de lire patiemment le traçage et de trouver l’indice de la cause. Bien entendu, le message d’erreur devrait également vous donner des conseils utiles.

Lectures complémentaires

Cette section fournit plus de ressources sur le sujet si vous souhaitez approfondir.

Livres

  • Python Cookbook, 3e édition par David Beazley et Brian K. Jones

Documentation officielle de Python

  • Le module de traçabilité dans la bibliothèque standard Python

Résumé

Dans ce tutoriel, vous avez découvert comment lire et imprimer le traçage d'un programme Python.

Concrètement, vous avez appris :

  • Quelles informations le traçage vous indique
  • Comment imprimer un traçage à tout moment de votre programme sans lever d'exception

Dans le prochain article, nous verrons comment naviguer dans la pile d'appels dans le débogueur Python.

Articles connexes