Un aperçu de la lecture des _tracebacks_ Python pour le débogage.
Lorsqu'une fonction est appelée, un objet frame est créé pour contenir les variables locales et les arguments passés à la fonction.
Quand la fonction se termine, l'objet frame est détruit.
Quand la fonction B est appelée à l'intérieur de la fonction A, les valeurs de la fonction B sont placées dans un objet frame, qui est ensuite empilé au-dessus de l'objet frame de la fonction A sur la pile d'appels.
La pile d'appels est un ensemble d'objets frame correspondant aux fonctions actuellement actives.
Si la fonction A a appelé la fonction B et que la fonction B a appelé la fonction C, alors les objets frame de ces trois fonctions se trouvent sur la pile d'appels.
Une fois que la fonction C se termine, son objet frame est dépilé de la pile et seuls les objets frame des fonctions A et B restent sur la pile d'appels.
Une trace d'appels est un rapport de tous les objets frame présents sur la pile à un instant donné. Quand un programme Python rencontre une exception non gérée, il affiche le message de l'exception et une trace d'appels. La trace d'appels montre où l'exception a été levée et quelles fonctions ont été appelées jusque-là.
ValueError est une exception courante.
Voici un exemple de ValueError provoquée par la tentative d'affecter deux variables à gauche à partir d'une seule valeur à droite :
>>> first, second = [1]
Traceback (most recent call last):
File <stdin>, line 1, in <module>
first, second = [1]
ValueError: not enough values to unpack (expected 2, got 1)
Dans une trace d'appels, l'appel le plus récent est le dernier : il faut donc commencer la lecture par l'exception, tout en bas. En remontant à partir de là, on voit comment cette instruction a été atteinte. Si on place la ligne fautive dans une fonction puis qu'on appelle cette fonction, on obtient une trace plus longue :
>>> def my_func():
... first, second = [1]
...
>>> my_func()
Traceback (most recent call last):
File <stdin>, line 5, in <module>
my_func()
File <stdin>, line 2, in my_func
first, second = [1]
ValueError: not enough values to unpack (expected 2, got 1)
En remontant depuis le bas, on voit que l'appel où l'exception s'est produite se trouve à la ligne 2, dans my_func.
On y est arrivé en appelant my_func à la ligne 5.
Python définit plus de 60 classes d'exceptions intégrées. Voici un aperçu rapide de quelques-unes des exceptions les plus courantes et de ce qu'elles indiquent.
Python lève une SyntaxError quand il n'arrive pas à comprendre le code à cause d'une syntaxe invalide.
Par exemple, il peut y avoir une parenthèse ouvrante sans parenthèse fermante correspondante.
Si on exécute ce code :
def distance(strand_a, strand_b):
if len(strand_a) != len(strand_b):
raise ValueError("Strands must be of equal length." # This is missing the closing parenthesis
on obtient une trace de pile similaire à celle-ci (note le message de la dernière ligne) :
.usr.local.lib.python3.10.site-packages._pytest.python.py:608: in _importtestmodule
mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
.usr.local.lib.python3.10.site-packages._pytest.pathlib.py:533: in import_path
importlib.import_module(module_name)
.usr.local.lib.python3.10.importlib.__init__.py:126: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1050: in _gcd_import ???
<frozen importlib._bootstrap>:1027: in _find_and_load ???
<frozen importlib._bootstrap>:1006: in _find_and_load_unlocked ???
<frozen importlib._bootstrap>:688: in _load_unlocked ???
.usr.local.lib.python3.10.site-packages._pytest.assertion.rewrite.py:168: in exec_module
exec(co, module.__dict__)
.mnt.exercism-iteration.hamming_test.py:3: in <module>
from hamming import (
E File ".mnt.exercism-iteration.hamming.py", line 10
E raise ValueError("Strands must be of equal length."
E ^
E SyntaxError: '(' was never closed
Python lève une AssertionError quand une instruction assert (voir plus bas) échoue.
Si on exécute ce code :
def distance(strand_a, strand_b):
assert len(strand_a) == len(strand_b)
distance("ab", "abc")
on obtient une trace de pile similaire à celle-ci (note le message de la dernière ligne) :
hamming_test.py:3: in <module>
from hamming import (
hamming.py:5: in <module>
distance("ab", "abc")
hamming.py:2: in distance
assert len(strand_a) == len(strand_b)
E AssertionError
Une AttributeError est levée quand du code (ou un test unitaire !) essaie d'accéder à l'attribut d'un objet, alors que cet objet n'a pas un tel attribut.
Par exemple, un test unitaire s'attend à ce qu'un objet Robot possède un attribut direction, mais quand il essaie d'accéder à robot.direction, cet attribut n'existe pas.
Cela peut aussi signaler une faute de frappe, comme utiliser "Hello".lowercase() alors que la syntaxe correcte est "Hello".lower(). "Hello".lowercase() lève AttributeError: 'str' object has no attribute 'lowercase'.
Si on exécute ce code :
class Robot:
def __init__():
#note that there is no self.direction listed here
self.position = (0, 0)
self.orientation = 'SW'
def forward():
pass
robby = Robot
robby.direction
on obtient une trace de pile similaire à celle-ci (note le message de la dernière ligne) :
robot_simulator_test.py:3: in <module>
from robot_simulator import (
robot_simulator.py:12: in <module>
robby.direction
E AttributeError: type object 'Robot' has no attribute 'direction'
Si on exécute ce code :
def distance(strand_a, strand_b):
if strand_a.lowercase() == strand_b:
return 0
distance("ab", "abc")
on obtient une trace de pile similaire à celle-ci (note le message de la dernière ligne) :
def distance(strand_a, strand_b):
> if strand_a.lowercase() == strand_b:
E AttributeError: 'str' object has no attribute 'lowercase'
Une ImportError est levée quand du code essaie d'importer quelque chose, mais que Python n'y arrive pas.
Par exemple, un test unitaire pour Guidos Gorgeous Lasagna fait from lasagna import bake_time_remaining, mais le fichier de solution lasgana.py n'a peut-être pas défini bake_time_remaining.
Si on exécute le fichier lasgana.py sans la fonction définie, on obtient l'erreur suivante :
We received the following error when we ran your code:
ImportError while importing test module '.mnt.exercism-iteration.lasagna_test.py'.
Hint: make sure your test modules.packages have valid Python names.
Traceback:
.mnt.exercism-iteration.lasagna_test.py:6: in <module>
from lasagna import (EXPECTED_BAKE_TIME,
E ImportError: cannot import name 'bake_time_remaining' from 'lasagna' (.mnt.exercism-iteration.lasagna.py)
During handling of the above exception, another exception occurred:
.usr.local.lib.python3.10.importlib.__init__.py:126: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
.mnt.exercism-iteration.lasagna_test.py:23: in <module>
raise ImportError("In your 'lasagna.py' file, we can not find or import the"
E ImportError: In your 'lasagna.py' file, we can not find or import the function named 'bake_time_remaining()'. Did you mis-name or forget to define it?
### **IndexError**
Python raises an `IndexError` when an invalid index is used to look up a value in a sequence.
This often indicates the index is not computed properly and is often an off-by-one error.
<details>
<summary>Click here for code example</summary>
Consider the following code.
```python
def distance(strand_a, strand_b):
same = 0
for i in range(len(strand_a)):
if strand_a[i] == strand_b[i]:
same += 1
return same
distance("abc", "ab") # Note the first strand is longer than the second strand.
Si on exécute ce code, on obtient une erreur similaire à celle-ci. (Note la dernière ligne.)
hamming_test.py:3: in <module>
from hamming import (
hamming.py:9: in <module>
distance("abc", "ab") # Note the first strand is longer than the second strand.
hamming.py:4: in distance
if strand_a[i] == strand_b[i]:
E IndexError: string index out of range
Comme pour IndexError, cette exception est levée lorsqu'on utilise une clé pour rechercher une valeur dans un dictionnaire, mais que cette clé n'est pas définie dans le dictionnaire.
Prenons le code suivant.
def to_rna(dna_letter):
translation = {"G": "C", "C": "G", "A": "U", "T": "A"}
return translation[dna_letter]
print(to_rna("Q")) # Note, "Q" is not in the translation.
Si on exécute ce code, on obtient une erreur similaire à celle-ci. (Note la dernière ligne.)
rna_transcription_test.py:3: in <module>
from rna_transcription import to_rna
rna_transcription.py:6: in <module>
print(to_rna("Q"))
rna_transcription.py:3: in to_rna
return translation[dna_letter]
E KeyError: 'Q'
En général, une TypeError est levée quand un type de données incorrect est passé à une fonction ou utilisé dans une opération.
Prenons le code suivant.
def hello(name): # This function expects a string.
return 'Hello, ' + name + '!'
print(hello(100)) # 100 is not a string.
Si on exécute ce code, on obtient une erreur similaire à celle-ci. (Note la dernière ligne.)
hello_world_test.py:3: in <module>
import hello_world
hello_world.py:5: in <module>
print(hello(100))
hello_world.py:2: in hello
return 'Hello, ' + name + '!'
E TypeError: can only concatenate str (not "int") to str
Une ValueError est généralement levée quand une valeur invalide est passée à une fonction.
À noter que les racines carrées réelles n'existent que pour les nombres positifs.
Appeler math.sqrt(-1) lève ValueError: math domain error, car -1 n'est pas une valeur valide pour une racine carrée.
En termes techniques (mathématiques), -1 n'appartient pas au domaine des racines carrées.
import math
math.sqrt(-1)
Si on exécute ce code, on obtient une erreur similaire à celle-ci. (Note la dernière ligne.)
square_root_test.py:3: in <module>
from square_root import (
square_root.py:3: in <module>
math.sqrt(-1)
E ValueError: math domain error
print
Parfois, aucune erreur n'est levée, mais une valeur n'est pas celle attendue. Cela peut être particulièrement déroutant quand la valeur est le résultat d'une suite de calculs. Dans ce cas, il peut être utile d'examiner la valeur à chaque étape pour voir quelle étape ne se comporte pas comme prévu. La fonction print peut servir à afficher la valeur dans la console. Voici un exemple de fonction qui ne renvoie pas la valeur attendue :
# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
return (num / 2) * 4
Lorsqu'on passe 5 à la fonction, la valeur attendue est 8, mais elle renvoie 10.0.
Pour comprendre d'où vient le problème, on décompose le calcul afin de pouvoir inspecter la valeur à chaque étape.
# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
# verify the number in is what is expected
# prints 5
print(num)
# we want the int divided by an integer to be an integer
# but this prints 2.5! We've found our mistake.
print(num / 2)
# this makes sense, since 2.5 x 4 = 10.0
print((num / 2) * 4)
return (num / 2) * 4
What the `print` calls revealed is that we used `/` when we should have used `//`, the [floor division operator][floor division operator].
## Logging
[Logging][logging] can be used similarly to `print`, but it is more powerful.
What is logged can be configured by the logging severity (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.)
A call to the `logging.error` function can pass `True` to the `exc_info` parameter, which will additionally log the stack trace.
By configuring multiple handlers, logging can write to more than one place with the same logging function.
Following is an example of logging printed to the console:
```python
>>> import logging
>>>
>>> # configures minimum logging level as INFO
>>> logging.basicConfig(level=logging.INFO)
>>>
>>> def halve_and_quadruple(num):
... # prints INFO:root: num == 5
... logging.info(f" num == {num}")
... return (num // 2) * 4
...
>>> print(halve_and_quadruple(5))
Le niveau est configuré sur INFO parce que le niveau par défaut est WARNING.
Pour obtenir un journal persistant, on peut configurer le logger pour qu'il écrive dans un fichier, comme ceci :
>>> import logging
...
>>> # configures the output file name to example.log, and the minimum logging level as INFO
>>> logging.basicConfig(filename='example.log', level=logging.INFO)
...
... def halve_and_quadruple(num):
... # prints INFO:root: num == 5 to the example.log file
... logging.info(f" num == {num}")
... return (num // 2) * 4
...
>>> print(halve_and_quadruple(5))
assert est une instruction qui doit toujours être évaluée à True, sauf s'il y a un bug dans le programme.
Quand un assert est évalué à False, il lève une AssertionError.
La trace d'appels de l'AssertionError peut inclure un message facultatif qui fait partie de l'instruction assert.
Même si le message est facultatif, il est de bonne pratique d'en inclure toujours un dans la définition de l'assert.
Voici un exemple d'utilisation de assert :
>>> def int_division(dividend, divisor):
... assert divisor != 0, "divisor must not be 0"
... return dividend // divisor
...
>>> print(int_division(2, 1))
2
>>> print(int_division(2, 0))
Traceback (most recent call last):
File <stdin>, line 7, in <module>
print(int_division(2, 0))
^^^^^^^^^^^^^^^^^^
File <stdin>, line 2, in int_division
assert divisor != 0, "divisor must not be 0"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: divisor must not be 0
Si on commence à lire la trace d'appels par le bas (comme il se doit), on voit vite que le problème vient de ce que 0 ne devrait pas être passé comme divisor.
On peut aussi utiliser assert pour vérifier qu'une valeur est du type attendu :
>>> import numbers
...
...
... def int_division(dividend, divisor):
... assert divisor != 0, "divisor must not be 0"
... assert isinstance(divisor, numbers.Number), "divisor must be a number"
... return dividend // divisor
...
>>> print(int_division(2, 1))
2
>>> print(int_division(2, '0'))
Traceback (most recent call last):
File <stdin>, line 11, in <module>
print(int_division(2, '0'))
^^^^^^^^^^^^^^^^^^^^
File <stdin>, line 6, in int_division
assert isinstance(divisor, numbers.Number), "divisor must be a number"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: divisor must be a number
Une fois le bug identifié, envisage de remplacer l'assert par une gestion des erreurs.
En effet, toutes les instructions assert peuvent être désactivées en lançant Python avec les options -O ou -OO, ou en définissant la variable d'environnement PYTHONOPTIMIZE sur 1 ou 2.
Définir PYTHONOPTIMIZE sur 1 équivaut à lancer Python avec l'option -O, ce qui désactive les assertions.
Définir PYTHONOPTIMIZE sur 2 équivaut à lancer Python avec l'option -OO, ce qui désactive les assertions et supprime aussi les docstrings du bytecode.
Réduire le bytecode est une façon de faire tourner le code plus vite.
Python dispose d'un débogueur intégré, pdb.
Il permet d'exécuter le code pas à pas et d'inspecter les variables.
On peut aussi y définir des points d'arrêt.
Pour commencer, il faut d'abord faire import pdb, puis appeler pdb.set_trace() à l'endroit où on veut démarrer le débogage :
import pdb
def add(num1, num2):
return num1 + num2
pdb.set_trace()
sum = add(1,5)
print(sum)
Si on exécute ce code, on obtient une invite pdb dans laquelle on peut saisir des commandes.
Tape help pour obtenir la liste des commandes.
Les plus courantes sont step, qui entre dans la fonction appelée à cette ligne.
next passe par-dessus un appel de fonction et passe à la ligne suivante. where indique à quelle ligne on se trouve.
D'autres commandes utiles sont whatis <variable>, qui indique le type d'une variable, et print(<variable>), qui affiche la valeur d'une variable.
On peut aussi simplement utiliser <variable> pour afficher la valeur d'une variable.
Une autre commande est jump <line number>, qui saute à un numéro de ligne précis.
Voici un petit exemple d'utilisation du débogueur, à partir du code précédent. À noter que, pour cet exemple et les suivants, les plateformes MacOS ou Linux auraient des chemins de fichiers avec des barres obliques :
>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> step
... > c:\pdb.py(3)add()
... -> def add(num1, num2):
... (Pdb)
>>> whatis num1
... <class 'int'>
>>> print(num2)
... 5
>>> next
... > c:\pdb.py(4)add()
... -> return num1 + num2
... (Pdb)
>>> jump 3
... > c:\pdb.py(3)add()
... -> def add(num1, num2):
... (Pdb)
Les points d'arrêt se définissent avec break <filename>:<line number> <condition>, où la condition est une condition facultative qui doit être vraie pour que le point d'arrêt soit atteint.
Il suffit d'écrire break pour obtenir la liste des points d'arrêt définis.
Pour désactiver un point d'arrêt, on peut écrire disable <breakpoint number>.
Pour activer un point d'arrêt, on peut écrire enable <breakpoint number>.
Pour supprimer un point d'arrêt, on peut écrire clear <breakpoint number>.
Pour poursuivre l'exécution, on peut écrire continue ou c. Pour quitter le débogueur, on peut écrire quit ou q.
Voici un exemple d'utilisation des commandes de débogage ci-dessus, à partir du code précédent :
>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> break
...
>>> break pdb:4
... Breakpoint 1 at c:\pdb.py:4
>>> break
... Num Type Disp Enb Where
... 2 breakpoint keep yes at c:\pdb.py:4
>>> c # continue
... > c:\pdn.py(4)add()
... -> return num1 + num2
>>> disable break 1
... Disabled breakpoint 1 at c:\pdb.py:4
>>> break
... Num Type Disp Enb Where
... 1 breakpoint keep no at c:\pdb.py:4
... breakpoint already hit 1 time
>>> clear break 1
... Deleted breakpoint 1 at c:\pdb.py:4
>>> break
...
À partir de Python 3.7, il existe un moyen plus simple de créer des points d'arrêt.
Il suffit d'écrire breakpoint() à l'endroit voulu pour en créer un.
def add(num1, num2):
breakpoint()
return num1 + num2
breakpoint()
sum = add(1,5)
print(sum)
>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> c # continue
... > c:\pdb.py(5)add()
... -> return num1 + num2