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:
- La hiérarchie des appels d'un programme simple
- Retraçage en cas d'exception
- Déclencher le traçage manuellement
- 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.