Como ler tracebacks

Uma visão geral de como ler tracebacks do Python para depurar.


Objeto de frame

Quando uma função é chamada, é criado um objeto de frame para guardar as variáveis locais e os argumentos passados à função. Quando a função devolve, o objeto de frame é destruído. Quando a função B é chamada dentro da função A, os valores da função B são colocados num objeto de frame, que é depois colocado por cima do objeto de frame da função A na pilha de chamadas.

Pilha de chamadas

A pilha de chamadas é uma coleção de objetos de frame das funções que estão ativas nesse momento. Se a função A chamou a função B e a função B chamou a função C, então os objetos de frame das três funções estarão na pilha de chamadas. Assim que a função C devolve, o respetivo objeto de frame é retirado da pilha e só os objetos de frame das funções A e B permanecem na pilha de chamadas.

Traceback

Um traceback é um relatório de todos os objetos de frame que estão na pilha num determinado momento. Quando um programa Python encontra uma exceção não tratada, imprime a mensagem da exceção e um traceback. O traceback mostra onde a exceção foi lançada e que funções foram chamadas até se chegar a esse ponto.

Como ler um traceback

ValueError é uma exceção comum.

O seguinte é um exemplo de ValueError resultante de tentar atribuir duas variáveis à esquerda a partir de um único valor à direita:

>>> 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)

Os tracebacks estão organizados com a chamada mais recente em último lugar, por isso o sítio para começar a ler é a exceção, no fundo. Ao ler de baixo para cima a partir daí, conseguimos ver como se chegou a essa instrução. Se colocarmos a linha problemática numa função e depois chamarmos a função, vemos um traceback mais longo:

>>> 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)

A recuar a partir do fundo, vemos que a chamada onde aconteceu a exceção está na linha 2, dentro de my_func. Chegámos lá ao chamar my_func na linha 5.

Exceções comuns

O Python define mais de 60 classes de exceções incorporadas. Eis uma breve visão geral de algumas das exceções mais comuns e do que indicam.

SyntaxError

O Python lança um SyntaxError quando não consegue compreender o código devido a sintaxe inválida. Por exemplo, pode haver um parêntese de abertura sem o parêntese de fecho correspondente.

Clica aqui para ver um exemplo de código.

Ao correr 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

irá produzir um stack trace semelhante a este (repara na mensagem da última linha):

.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

AssertionError

O Python lança um AssertionError quando uma instrução assert (ver abaixo) falha.

Clica aqui para ver um exemplo de código.

Ao correr este código:

def distance(strand_a, strand_b):
    assert len(strand_a) == len(strand_b)


distance("ab", "abc")

irá produzir um stack trace semelhante a este (repara na mensagem da última linha):

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

AttributeError

Um AttributeError é lançado quando o código (ou um teste unitário!) tenta aceder a um atributo de um objeto, mas esse objeto não tem esse atributo. Por exemplo, um teste unitário espera que um objeto Robot tenha um atributo direction, mas quando tenta aceder a robot.direction, este não existe.

Isto também pode indicar um erro tipográfico, como usar "Hello".lowercase() quando a sintaxe correta é "Hello".lower(). "Hello".lowercase() lança AttributeError: 'str' object has no attribute 'lowercase'.

Clica aqui para ver um exemplo com a classe Robot.

Ao correr 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

irá produzir um stack trace semelhante a este (repara na mensagem da última linha):

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'
Clica aqui para ver um exemplo com uma string.

Ao correr este código:

def distance(strand_a, strand_b):
    if strand_a.lowercase() == strand_b:
        return 0


distance("ab", "abc")

irá produzir um stack trace semelhante a este (repara na mensagem da última linha):

    def distance(strand_a, strand_b):
>       if strand_a.lowercase() == strand_b:
E       AttributeError: 'str' object has no attribute 'lowercase'

ImportError

Um ImportError é lançado quando o código tenta importar algo, mas o Python não consegue fazê-lo. Por exemplo, um teste unitário para Guidos Gorgeous Lasagna faz from lasagna import bake_time_remaining, mas o ficheiro de solução lasgana.py pode não definir bake_time_remaining.

Clica aqui para ver um exemplo de código

Correr o ficheiro lasgana.py sem a função definida iria originar o seguinte erro:

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.

Correr esse código irá originar um erro semelhante a este. (Repara na última linha.)

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

KeyError

Tal como o IndexError, esta exceção é lançada quando se usa uma chave para procurar um valor num dicionário, mas a chave não está definida no dicionário.

Clica aqui para ver um exemplo de código

Considera o seguinte 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.

Correr esse código irá originar um erro semelhante a este. (Repara na última linha.)

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'

TypeError

Normalmente, um TypeError é lançado quando é passado à função, ou usado numa operação, um tipo de dados errado.

Clica aqui para ver um exemplo de código

Considera o seguinte código.

def hello(name):  # This function expects a string.
    return 'Hello, ' + name + '!'


print(hello(100))  # 100 is not a string.

Correr esse código irá originar um erro semelhante a este. (Repara na última linha.)

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

ValueError

Um ValueError é normalmente lançado quando é passado um valor inválido a uma função.

Clica aqui para ver um exemplo de código

Nota que as raízes quadradas reais só existem para números positivos. Chamar math.sqrt(-1) lança ValueError: math domain error, uma vez que -1 não é um valor válido para uma raiz quadrada. Em termos técnicos (matemáticos), o -1 não pertence ao domínio das raízes quadradas.

import math

math.sqrt(-1)

Correr esse código irá originar um erro semelhante a este. (Repara na última linha.)

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

Usar a função print

Por vezes, não é lançado nenhum erro, mas um valor não é o que se esperava. Isto pode ser especialmente desconcertante se o valor for o resultado de uma cadeia de cálculos. Numa situação dessas, pode ser útil observar o valor em cada passo para ver qual é o passo que não se está a comportar como esperado. A função print pode ser usada para imprimir o valor na consola. O seguinte é um exemplo de uma função que não devolve o 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

Quando se passa 5 à função, o valor esperado é 8, mas esta devolve 10.0. Para diagnosticar o problema, o cálculo é dividido em partes, para que o valor possa ser inspecionado em cada 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))

O nível é configurado como INFO porque o nível predefinido é WARNING. Para um registo persistente, o logger pode ser configurado para escrever num ficheiro, assim:

>>> 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

Um assert é uma instrução que deve ser sempre avaliada como True, a menos que haja um bug no programa. Quando um assert é avaliado como False, lança um AssertionError. O traceback do AssertionError pode incluir uma mensagem opcional que faz parte da instrução assert. Embora a mensagem seja opcional, é boa prática incluir sempre uma na definição do assert.

O seguinte é um exemplo de utilização do 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 começarmos a ler o traceback a partir do fundo (como devemos), vemos rapidamente que o problema é que 0 não deve ser passado como divisor.

O assert também pode ser usado para testar se um valor é do 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

Depois de identificares um bug, considera substituir o assert por tratamento de erros. Isto acontece porque todas as instruções assert podem ser desativadas ao correr o Python com as opções -O ou -OO, ou ao definir a variável de ambiente PYTHONOPTIMIZE como 1 ou 2. Definir PYTHONOPTIMIZE como 1 é equivalente a correr o Python com a opção -O, que desativa as asserções. Definir PYTHONOPTIMIZE como 2 é equivalente a correr o Python com a opção -OO, que desativa as asserções e remove as docstrings do bytecode. Reduzir o bytecode é uma forma de fazer o código correr mais depressa.

Depurador do Python

O Python tem um depurador incorporado, o pdb. Pode ser usado para percorrer o código passo a passo e inspecionar variáveis. Também podes definir breakpoints com ele. Para começar, tens primeiro de fazer import pdb e depois chamar pdb.set_trace() no sítio onde queres começar a depurar:

import pdb

def add(num1, num2):
    return num1 + num2

pdb.set_trace()
sum = add(1,5)
print(sum)

Correr este código dá-te um prompt do pdb onde podes escrever comandos. Escreve help para obter uma lista de comandos. Os mais comuns são step, que entra na função chamada nessa linha. next passa por cima de uma chamada de função e avança para a linha seguinte. where indica em que linha estás. Alguns outros comandos úteis são whatis <variable>, que indica o tipo de uma variável, e print(<variable>), que imprime o valor de uma variável. Também podes simplesmente usar <variable> para imprimir o valor de uma variável. Outro comando é jump <line number>, que salta para um número de linha específico.

Eis um pequeno exemplo de como usar o depurador, com base no código anterior. Nota que, para este e os exemplos seguintes, as plataformas macOS ou Linux teriam caminhos de ficheiro com 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)

Os breakpoints configuram-se com break <filename>:<line number> <condition>, em que a condição é uma condição opcional que tem de ser verdadeira para o breakpoint ser atingido. Podes simplesmente escrever break para obter uma lista dos breakpoints que definiste. Para desativar um breakpoint, podes escrever disable <breakpoint number>. Para ativar um breakpoint, podes escrever enable <breakpoint number>. Para eliminar um breakpoint, podes escrever clear <breakpoint number>. Para continuar a execução, podes escrever continue ou c. Para sair do depurador, podes escrever quit ou q.

Eis um exemplo de como usar os comandos do depurador acima, com base no 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
...

No Python 3.7+ há uma forma mais fácil de criar breakpoints. Basta escrever breakpoint() onde for necessário para criar um.

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