Una visión general de cómo leer los tracebacks de Python para depurar.
Cuando se llama a una función, se crea un objeto de marco para guardar las variables locales y los argumentos que se pasan a la función.
Cuando la función regresa, el objeto de marco se destruye.
Cuando se llama a la función B dentro de la función A, los valores de la función B se colocan en un objeto de marco, que luego se pone encima del objeto de marco de la función A en la pila de llamadas.
La pila de llamadas es una colección de objetos de marco de las funciones que están activas en ese momento.
Si la función A ha llamado a la función B y la función B ha llamado a la función C, entonces los objetos de marco de las tres funciones estarán en la pila de llamadas.
Una vez que la función C regresa, su objeto de marco se saca de la pila y solo quedan en la pila de llamadas los objetos de marco de las funciones A y B.
Un traceback es un informe de todos los objetos de marco que hay en la pila en un momento específico. Cuando un programa de Python se encuentra con una excepción no controlada, imprime el mensaje de la excepción y un traceback. El traceback muestra dónde se lanzó la excepción y qué funciones se llamaron antes de llegar a ella.
ValueError es una excepción común.
El siguiente es un ejemplo de ValueError que resulta de intentar asignar dos variables a la izquierda a partir de un solo valor a la derecha:
>>> 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)
Los tracebacks se organizan con la llamada más reciente al final, así que el lugar por donde hay que empezar a leer el traceback es la excepción que está abajo. Si leemos hacia arriba desde ahí, podemos ver cómo se llegó a esa sentencia. Si colocamos la línea problemática dentro de una función y luego llamamos a la función, vemos un trace más largo:
>>> 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)
Trabajando hacia atrás desde abajo, vemos que la llamada donde ocurrió la excepción está en la línea 2, dentro de my_func.
Llegamos ahí porque llamamos a my_func en la línea 5.
Python define más de 60 clases de excepciones integradas. Aquí tienes una breve descripción general de algunas de las excepciones más comunes y qué indican.
Python lanza un SyntaxError cuando no puede entender el código debido a una sintaxis no válida.
Por ejemplo, puede haber un paréntesis de apertura sin su paréntesis de cierre correspondiente.
Si ejecutas este código:
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
obtendrás un traceback similar a este (ten en cuenta el mensaje de la última línea):
.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 lanza un AssertionError cuando falla una sentencia assert (ver más abajo).
Si ejecutas este código:
def distance(strand_a, strand_b):
assert len(strand_a) == len(strand_b)
distance("ab", "abc")
obtendrás un traceback similar a este (ten en cuenta el mensaje de la última línea):
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
Se lanza un AttributeError cuando el código (¡o una prueba unitaria!) intenta acceder al atributo de un objeto, pero ese objeto no tiene tal atributo.
Por ejemplo, una prueba unitaria espera que un objeto Robot tenga un atributo direction, pero cuando intenta acceder a robot.direction, este no existe.
Esto también podría indicar un error de escritura, como usar "Hello".lowercase() cuando la sintaxis correcta es "Hello".lower(). "Hello".lowercase() lanza AttributeError: 'str' object has no attribute 'lowercase'.
Si ejecutas este código:
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
obtendrás un traceback similar a este (ten en cuenta el mensaje de la última línea):
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 ejecutas este código:
def distance(strand_a, strand_b):
if strand_a.lowercase() == strand_b:
return 0
distance("ab", "abc")
obtendrás un traceback similar a este (ten en cuenta el mensaje de la última línea):
def distance(strand_a, strand_b):
> if strand_a.lowercase() == strand_b:
E AttributeError: 'str' object has no attribute 'lowercase'
Se lanza un ImportError cuando el código intenta importar algo, pero Python no puede hacerlo.
Por ejemplo, una prueba unitaria de Guidos Gorgeous Lasagna hace from lasagna import bake_time_remaining, pero puede que el archivo de solución lasgana.py no defina bake_time_remaining.
Ejecutar el archivo lasgana.py sin la función definida daría como resultado el siguiente error:
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 ejecutas ese código, obtendrás un error similar a este. (Ten en cuenta la última línea.)
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
Al igual que IndexError, esta excepción se lanza cuando se usa una clave para buscar un valor en un diccionario, pero la clave no está definida en el diccionario.
Considera el siguiente código.
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 ejecutas ese código, obtendrás un error similar a este. (Ten en cuenta la última línea.)
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'
Por lo general, se lanza un TypeError cuando el tipo de dato equivocado se pasa a una función o se usa en una operación.
Considera el siguiente código.
def hello(name): # This function expects a string.
return 'Hello, ' + name + '!'
print(hello(100)) # 100 is not a string.
Si ejecutas ese código, obtendrás un error similar a este. (Ten en cuenta la última línea.)
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
Por lo general, se lanza un ValueError cuando se pasa un valor no válido a una función.
Ten en cuenta que las raíces cuadradas reales solo existen para números positivos.
Llamar a math.sqrt(-1) lanzará ValueError: math domain error, ya que -1 no es un valor válido para una raíz cuadrada.
En términos técnicos (matemáticos), -1 no está en el dominio de las raíces cuadradas.
import math
math.sqrt(-1)
Si ejecutas ese código, obtendrás un error similar a este. (Ten en cuenta la última línea.)
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 veces no se lanza un error, pero un valor no es el esperado. Esto puede ser especialmente desconcertante si el valor es el resultado de una cadena de cálculos. En tal situación, puede ser útil mirar el valor en cada paso para ver cuál es el paso que no se comporta como se espera. La función print se puede usar para imprimir el valor en la consola. El siguiente es un ejemplo de una función que no devuelve el valor esperado:
# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
return (num / 2) * 4
Cuando se pasa 5 a la función, el valor esperado es 8, pero devuelve 10.0.
Para solucionar el problema, el cálculo se divide para poder inspeccionar el valor en cada paso.
# 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))
El nivel se configura como INFO porque el nivel predeterminado es WARNING.
Para un registro persistente, el logger se puede configurar para escribir en un archivo, así:
>>> 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 es una sentencia que siempre debería evaluarse como True, a menos que haya un error en el programa.
Cuando un assert se evalúa como False, lanza un AssertionError.
El traceback del AssertionError puede incluir un mensaje opcional que forma parte de la sentencia assert.
Aunque el mensaje es opcional, es una buena práctica incluir siempre uno en la definición del assert.
El siguiente es un ejemplo del uso 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 empezamos a leer el traceback desde abajo (como deberíamos), vemos rápidamente que el problema es que no se debería pasar 0 como divisor.
assert también se puede usar para comprobar que un valor es del tipo esperado:
>>> 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 vez que se identifica un error, considera reemplazar el assert por el manejo de errores.
Esto se debe a que todas las sentencias assert se pueden deshabilitar ejecutando Python con las opciones -O o -OO, o estableciendo la variable de entorno PYTHONOPTIMIZE en 1 o 2.
Establecer PYTHONOPTIMIZE en 1 equivale a ejecutar Python con la opción -O, que deshabilita las aserciones.
Establecer PYTHONOPTIMIZE en 2 equivale a ejecutar Python con la opción -OO, que deshabilita las aserciones y elimina los docstrings del bytecode.
Reducir el bytecode es una forma de hacer que el código se ejecute más rápido.
Python tiene un depurador integrado, pdb.
Se puede usar para recorrer el código paso a paso e inspeccionar variables.
También puedes establecer puntos de interrupción con él.
Para empezar, primero tienes que hacer import pdb y luego llamar a pdb.set_trace() donde quieras empezar a depurar:
import pdb
def add(num1, num2):
return num1 + num2
pdb.set_trace()
sum = add(1,5)
print(sum)
Ejecutar este código te dará un indicador de pdb donde puedes escribir comandos.
Escribe help para obtener una lista de comandos.
Los más comunes son step, que entra en una función llamada en esa línea.
next pasa por encima de una llamada a función y avanza a la siguiente línea. where te dice en qué línea estás.
Algunos otros comandos útiles son whatis <variable>, que te dice el tipo de una variable, y print(<variable>), que imprime el valor de una variable.
También puedes usar simplemente <variable> para imprimir el valor de una variable.
Otro comando es jump <line number>, que salta a un número de línea específico.
Aquí tienes un pequeño ejemplo de cómo usar el depurador basado en el código anterior. Ten en cuenta que, para este y los siguientes ejemplos, en las plataformas MacOS o Linux las rutas de archivo usarían barras inclinadas:
>>> 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)
Los puntos de interrupción se establecen con break <filename>:<line number> <condition>, donde la condición es una condición opcional que debe ser verdadera para que se alcance el punto de interrupción.
Puedes escribir simplemente break para obtener una lista de los puntos de interrupción que has establecido.
Para deshabilitar un punto de interrupción, puedes escribir disable <breakpoint number>.
Para habilitar un punto de interrupción, puedes escribir enable <breakpoint number>.
Para eliminar un punto de interrupción, puedes escribir clear <breakpoint number>.
Para continuar la ejecución, puedes escribir continue o c. Para salir del depurador, puedes escribir quit o q.
Aquí tienes un ejemplo de cómo usar los comandos del depurador anteriores basado en el código anterior:
>>> 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
...
En Python 3.7+ hay una forma más sencilla de crear puntos de interrupción.
Basta con escribir breakpoint() donde se necesite para crear 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