Una panoramica su come leggere i traceback di Python per il debug.
Quando viene chiamata una funzione, viene creato un oggetto frame che contiene le variabili locali e gli argomenti passati alla funzione.
Quando la funzione restituisce, l'oggetto frame viene distrutto.
Quando la funzione B viene chiamata all'interno della funzione A, i valori della funzione B vengono inseriti in un oggetto frame, che viene poi posto sopra l'oggetto frame della funzione A nello stack delle chiamate.
Lo stack delle chiamate è una raccolta di oggetti frame per le funzioni attualmente attive.
Se la funzione A ha chiamato la funzione B e la funzione B ha chiamato la funzione C, allora gli oggetti frame di tutte e tre le funzioni si troveranno sullo stack delle chiamate.
Una volta che la funzione C restituisce, il suo oggetto frame viene rimosso dallo stack e sullo stack delle chiamate rimarranno solo gli oggetti frame delle funzioni A e B.
Un traceback è un resoconto di tutti gli oggetti frame presenti sullo stack in un determinato momento. Quando un programma Python incontra un'eccezione non gestita, stampa il messaggio dell'eccezione e un traceback. Il traceback mostra dove è stata sollevata l'eccezione e quali funzioni sono state chiamate fino a quel punto.
ValueError è un'eccezione comune.
Il seguente è un esempio di ValueError che deriva dal tentativo di assegnare due variabili a sinistra partendo da un solo valore a destra:
>>> 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)
I traceback sono organizzati con la chiamata più recente in fondo, quindi il punto da cui iniziare a leggere il traceback è l'eccezione in basso. Leggendo verso l'alto da lì, possiamo vedere come si è arrivati a quell'istruzione. Se mettiamo la riga incriminata dentro una funzione e poi chiamiamo la funzione, otteniamo una traccia più lunga:
>>> 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)
Risalendo dal fondo, vediamo che la chiamata in cui si è verificata l'eccezione è alla riga 2 in my_func.
Ci siamo arrivati chiamando my_func alla riga 5.
Python definisce oltre 60 classi di eccezioni integrate. Ecco una breve panoramica di alcune delle eccezioni più comuni e di cosa indicano.
Python solleva un SyntaxError quando non riesce a capire il codice a causa di una sintassi non valida.
Per esempio, può esserci una parentesi aperta senza la corrispondente parentesi di chiusura.
Eseguendo questo codice:
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
si otterrà uno stack trace simile a questo (nota il messaggio sull'ultima riga):
.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 solleva un AssertionError quando un'istruzione assert (vedi sotto) fallisce.
Eseguendo questo codice:
def distance(strand_a, strand_b):
assert len(strand_a) == len(strand_b)
distance("ab", "abc")
si otterrà uno stack trace simile a questo (nota il messaggio sull'ultima riga):
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
Un AttributeError viene sollevato quando il codice (o un test unitario!) cerca di accedere all'attributo di un oggetto, ma quell'oggetto non ha tale attributo.
Per esempio, un test unitario si aspetta che un oggetto Robot abbia un attributo direction, ma quando tenta di accedere a robot.direction, questo non esiste.
Questo potrebbe anche indicare un errore di battitura, come usare "Hello".lowercase() quando la sintassi corretta è "Hello".lower(). "Hello".lowercase() solleva AttributeError: 'str' object has no attribute 'lowercase'.
Eseguendo questo codice:
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
si otterrà uno stack trace simile a questo (nota il messaggio sull'ultima riga):
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'
Eseguendo questo codice:
def distance(strand_a, strand_b):
if strand_a.lowercase() == strand_b:
return 0
distance("ab", "abc")
si otterrà uno stack trace simile a questo (nota il messaggio sull'ultima riga):
def distance(strand_a, strand_b):
> if strand_a.lowercase() == strand_b:
E AttributeError: 'str' object has no attribute 'lowercase'
Un ImportError viene sollevato quando il codice cerca di importare qualcosa, ma Python non è in grado di farlo.
Per esempio, un test unitario per Guidos Gorgeous Lasagna fa from lasagna import bake_time_remaining, ma il file di soluzione lasgana.py potrebbe non definire bake_time_remaining.
Eseguire il file lasgana.py senza la funzione definita darebbe il seguente errore:
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.
Eseguendo quel codice si otterrà un errore simile a questo. (Nota l'ultima riga.)
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
Simile a IndexError, questa eccezione viene sollevata quando si usa una chiave per cercare un valore in un dizionario, ma la chiave non è presente nel dizionario.
Considera il seguente codice.
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.
Eseguendo quel codice si otterrà un errore simile a questo. (Nota l'ultima riga.)
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'
Di solito, un TypeError viene sollevato quando a una funzione viene passato un tipo di dato sbagliato, oppure se ne usa uno sbagliato in un'operazione.
Considera il seguente codice.
def hello(name): # This function expects a string.
return 'Hello, ' + name + '!'
print(hello(100)) # 100 is not a string.
Eseguendo quel codice si otterrà un errore simile a questo. (Nota l'ultima riga.)
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
Un ValueError viene di solito sollevato quando a una funzione viene passato un valore non valido.
Nota: le radici quadrate reali esistono solo per i numeri positivi.
Chiamare math.sqrt(-1) solleva ValueError: math domain error, dato che -1 non è un valore valido per una radice quadrata.
In termini tecnici (matematici), -1 non appartiene al dominio delle radici quadrate.
import math
math.sqrt(-1)
Eseguendo quel codice si otterrà un errore simile a questo. (Nota l'ultima riga.)
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
A volte non viene sollevato alcun errore, ma un valore non è quello che ci si aspetta. Questo può essere particolarmente sconcertante se il valore è il risultato di una catena di calcoli. In una situazione del genere può essere utile osservare il valore a ogni passo per capire quale passo è quello che non si comporta come previsto. La funzione print può essere usata per stampare il valore sulla console. Il seguente è un esempio di una funzione che non restituisce il valore previsto:
# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
return (num / 2) * 4
Quando alla funzione viene passato 5, il valore previsto è 8, ma restituisce 10.0.
Per fare debugging, il calcolo viene suddiviso in modo da poter ispezionare il valore a ogni passo.
# 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))
Il livello è configurato come INFO perché il livello predefinito è WARNING.
Per un log persistente, il logger può essere configurato per scrivere su un file, così:
>>> 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 è un'istruzione che dovrebbe sempre dare come risultato True, a meno che non ci sia un bug nel programma.
Quando un assert dà come risultato False, solleva un AssertionError.
Il traceback per AssertionError può includere un messaggio facoltativo che fa parte dell'istruzione assert.
Sebbene un messaggio sia facoltativo, è buona pratica includerne sempre uno nella definizione di assert.
Il seguente è un esempio di utilizzo di 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
Se iniziamo a leggere il traceback dal fondo (come dovremmo), vediamo subito che il problema è che 0 non dovrebbe essere passato come divisor.
assert può anche essere usato per verificare che un valore sia del tipo previsto:
>>> 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
Una volta identificato un bug, valuta di sostituire l'assert con la gestione degli errori.
Questo perché tutte le istruzioni assert possono essere disabilitate eseguendo Python con le opzioni -O o -OO, oppure impostando la variabile d'ambiente PYTHONOPTIMIZE a 1 o 2.
Impostare PYTHONOPTIMIZE a 1 equivale a eseguire Python con l'opzione -O, che disabilita le asserzioni.
Impostare PYTHONOPTIMIZE a 2 equivale a eseguire Python con l'opzione -OO, che disabilita le asserzioni e rimuove le docstring dal bytecode.
Ridurre il bytecode è un modo per far girare il codice più velocemente.
Python ha un debugger integrato, pdb.
Può essere usato per eseguire il codice passo passo e ispezionare le variabili.
Puoi anche impostare dei breakpoint.
Per iniziare, devi prima fare import pdb e poi chiamare pdb.set_trace() dove vuoi iniziare il debugging:
import pdb
def add(num1, num2):
return num1 + num2
pdb.set_trace()
sum = add(1,5)
print(sum)
Eseguendo questo codice otterrai un prompt di pdb in cui puoi digitare dei comandi.
Scrivi help per ottenere un elenco di comandi.
I più comuni sono step, che entra in una funzione chiamata a quella riga.
next salta oltre una chiamata di funzione e passa alla riga successiva. where ti dice su quale riga ti trovi.
Altri comandi utili sono whatis <variable>, che ti dice il tipo di una variabile, e print(<variable>), che stampa il valore di una variabile.
Puoi anche semplicemente usare <variable> per stampare il valore di una variabile.
Un altro comando è jump <line number>, che salta a un numero di riga specifico.
Ecco un piccolo esempio di come usare il debugger, basato sul codice precedente. Nota che per questo e i seguenti esempi, le piattaforme MacOS o Linux avrebbero percorsi di file con barre oblique (/):
>>> 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)
I breakpoint si impostano con break <filename>:<line number> <condition>, dove la condizione è una condizione facoltativa che deve essere vera perché il breakpoint venga raggiunto.
Puoi semplicemente scrivere break per ottenere un elenco dei breakpoint impostati.
Per disabilitare un breakpoint puoi scrivere disable <breakpoint number>.
Per abilitare un breakpoint puoi scrivere enable <breakpoint number>.
Per eliminare un breakpoint puoi scrivere clear <breakpoint number>.
Per continuare l'esecuzione puoi scrivere continue o c. Per uscire dal debugger puoi scrivere quit o q.
Ecco un esempio di come usare i comandi del debugger precedenti, basato sul codice di prima:
>>> 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
...
In Python 3.7+ c'è un modo più semplice per creare breakpoint.
Scrivere semplicemente breakpoint() dove serve ne crea uno.
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