Wie du Tracebacks liest

Ein Überblick, wie du Python-Tracebacks zum Debuggen liest.


Frame-Objekt

Wenn eine Funktion aufgerufen wird, wird ein Frame-Objekt erstellt, das die lokalen Variablen und die an die Funktion übergebenen Argumente enthält. Wenn die Funktion zurückkehrt, wird das Frame-Objekt zerstört. Wenn die Funktion B innerhalb der Funktion A aufgerufen wird, werden die Werte der Funktion B in ein Frame-Objekt gelegt, das dann im Aufrufstack oben auf dem Frame-Objekt der Funktion A abgelegt wird.

Aufrufstack

Der Aufrufstack ist eine Sammlung von Frame-Objekten für die gerade aktiven Funktionen. Wenn die Funktion A die Funktion B aufgerufen hat und die Funktion B die Funktion C aufgerufen hat, dann liegen die Frame-Objekte aller drei Funktionen auf dem Aufrufstack. Sobald die Funktion C zurückkehrt, wird ihr Frame-Objekt vom Stack genommen, und nur die Frame-Objekte der Funktionen A und B bleiben auf dem Aufrufstack.

Traceback

Ein Traceback ist ein Bericht über alle Frame-Objekte auf dem Stack zu einem bestimmten Zeitpunkt. Wenn ein Python-Programm auf eine nicht behandelte Ausnahme stößt, gibt es die Ausnahmemeldung und einen Traceback aus. Der Traceback zeigt, wo die Ausnahme ausgelöst wurde und welche Funktionen bis dahin aufgerufen wurden.

So liest du einen Traceback

ValueError ist eine häufige Ausnahme.

Das folgende Beispiel zeigt einen ValueError, der entsteht, wenn du versuchst, zwei Variablen auf der linken Seite aus nur einem Wert auf der rechten Seite zuzuweisen:

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

Bei Tracebacks steht der jüngste Aufruf am Ende. Deshalb beginnst du beim Lesen am besten mit der Ausnahme ganz unten. Wenn du von dort nach oben liest, siehst du, wie diese Anweisung erreicht wurde. Wenn wir die fehlerhafte Zeile in eine Funktion packen und die Funktion dann aufrufen, sehen wir einen längeren Trace:

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

Wenn wir uns von unten nach oben arbeiten, sehen wir, dass der Aufruf, bei dem die Ausnahme auftrat, in Zeile 2 in my_func liegt. Dorthin gelangt sind wir durch den Aufruf von my_func in Zeile 5.

Häufige Ausnahmen

Python definiert über 60 eingebaute Ausnahmeklassen. Hier ist ein kurzer Überblick über einige der häufigeren Ausnahmen und was sie bedeuten.

SyntaxError

Python löst einen SyntaxError aus, wenn es den Code wegen ungültiger Syntax nicht verstehen kann. Zum Beispiel kann eine geöffnete Klammer ohne passende schließende Klammer vorhanden sein.

Klicke hier für ein Codebeispiel.

Wenn du diesen Code ausführst:

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

ergibt das einen Stacktrace, der dem folgenden ähnelt (beachte die Meldung in der letzten Zeile):

.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

Python löst einen AssertionError aus, wenn eine assert-Anweisung (siehe unten) fehlschlägt.

Klicke hier für ein Codebeispiel.

Wenn du diesen Code ausführst:

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


distance("ab", "abc")

ergibt das einen Stacktrace, der dem folgenden ähnelt (beachte die Meldung in der letzten Zeile):

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

Ein AttributeError wird ausgelöst, wenn Code (oder ein Unit-Test!) versucht, auf das Attribut eines Objekts zuzugreifen, aber das Objekt kein solches Attribut hat. Zum Beispiel erwartet ein Unit-Test, dass ein Robot-Objekt ein Attribut direction hat, aber wenn er auf robot.direction zugreift, existiert es nicht.

Das kann auch auf einen Tippfehler hindeuten, etwa wenn du "Hello".lowercase() verwendest, obwohl die korrekte Syntax "Hello".lower() lautet. "Hello".lowercase() löst AttributeError: 'str' object has no attribute 'lowercase' aus.

Klicke hier für ein Beispiel mit der Robot-Klasse.

Wenn du diesen Code ausführst:

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

ergibt das einen Stacktrace, der dem folgenden ähnelt (beachte die Meldung in der letzten Zeile):

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'
Klicke hier für ein Beispiel mit einem String.

Wenn du diesen Code ausführst:

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


distance("ab", "abc")

ergibt das einen Stacktrace, der dem folgenden ähnelt (beachte die Meldung in der letzten Zeile):

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

ImportError

Ein ImportError wird ausgelöst, wenn Code versucht, etwas zu importieren, aber Python dazu nicht in der Lage ist. Zum Beispiel führt ein Unit-Test für Guidos Gorgeous Lasagna from lasagna import bake_time_remaining aus, aber die Lösungsdatei lasgana.py definiert bake_time_remaining möglicherweise nicht.

Klicke hier für ein Codebeispiel

Wenn du die Datei lasgana.py ohne die definierte Funktion ausführst, ergibt sich der folgende Fehler:

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.

Wenn du diesen Code ausführst, ergibt sich ein Fehler, der dem folgenden ähnelt. (Beachte die letzte Zeile.)

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

Ähnlich wie beim IndexError wird diese Ausnahme ausgelöst, wenn du mit einem Schlüssel einen Wert in einem Wörterbuch nachschlägst, der Schlüssel aber nicht im Wörterbuch gesetzt ist.

Klicke hier für ein Codebeispiel

Betrachte den folgenden Code.

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.

Wenn du diesen Code ausführst, ergibt sich ein Fehler, der dem folgenden ähnelt. (Beachte die letzte Zeile.)

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

Ein TypeError wird typischerweise ausgelöst, wenn ein falscher Datentyp an eine Funktion übergeben oder in einer Operation verwendet wird.

Klicke hier für ein Codebeispiel

Betrachte den folgenden Code.

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


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

Wenn du diesen Code ausführst, ergibt sich ein Fehler, der dem folgenden ähnelt. (Beachte die letzte Zeile.)

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

Ein ValueError wird normalerweise ausgelöst, wenn ein ungültiger Wert an eine Funktion übergeben wird.

Klicke hier für ein Codebeispiel

Beachte, dass echte Quadratwurzeln nur für positive Zahlen existieren. Der Aufruf math.sqrt(-1) löst ValueError: math domain error aus, weil -1 kein gültiger Wert für eine Quadratwurzel ist. In der (mathematischen) Fachsprache ausgedrückt: -1 liegt nicht im Definitionsbereich der Quadratwurzel.

import math

math.sqrt(-1)

Wenn du diesen Code ausführst, ergibt sich ein Fehler, der dem folgenden ähnelt. (Beachte die letzte Zeile.)

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

Die Funktion print verwenden

Manchmal wird kein Fehler ausgelöst, aber ein Wert ist nicht der erwartete. Das kann besonders verwirrend sein, wenn der Wert das Ergebnis einer Kette von Berechnungen ist. In so einer Situation kann es helfen, den Wert bei jedem Schritt zu betrachten, um zu sehen, welcher Schritt sich nicht wie erwartet verhält. Mit der Funktion print kannst du den Wert auf der Konsole ausgeben. Das folgende Beispiel zeigt eine Funktion, die nicht den erwarteten Wert zurückgibt:

# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
    return (num / 2) * 4

Wenn der Funktion 5 übergeben wird, ist der erwartete Wert 8, aber sie gibt 10.0 zurück. Zur Fehlersuche wird die Berechnung aufgeteilt, damit der Wert bei jedem Schritt überprüft werden kann.

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

Der Level ist auf INFO gesetzt, weil der Standard-Level WARNING ist. Für ein dauerhaftes Log kannst du den Logger so konfigurieren, dass er in eine Datei schreibt:

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

assert ist eine Anweisung, die immer True ergeben sollte, es sei denn, es gibt einen Fehler im Programm. Wenn ein assert False ergibt, wird ein AssertionError ausgelöst. Der Traceback für den AssertionError kann eine optionale Meldung enthalten, die Teil der assert-Anweisung ist. Eine Meldung ist zwar optional, aber es ist gute Praxis, immer eine in die assert-Definition aufzunehmen.

Das folgende Beispiel zeigt die Verwendung von 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

Wenn wir den Traceback unten beginnen zu lesen (was wir tun sollten), sehen wir schnell, dass das Problem darin besteht, dass 0 nicht als divisor übergeben werden sollte.

Mit assert kannst du auch prüfen, ob ein Wert den erwarteten Typ hat:

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

Sobald ein Fehler gefunden wurde, solltest du erwägen, das assert durch Fehlerbehandlung zu ersetzen. Der Grund: Alle assert-Anweisungen kannst du deaktivieren, indem du Python mit den Optionen -O oder -OO ausführst oder die Umgebungsvariable PYTHONOPTIMIZE auf 1 oder 2 setzt. Wenn du PYTHONOPTIMIZE auf 1 setzt, entspricht das dem Ausführen von Python mit der Option -O, die Assertions deaktiviert. Wenn du PYTHONOPTIMIZE auf 2 setzt, entspricht das dem Ausführen von Python mit der Option -OO, die sowohl Assertions deaktiviert als auch Docstrings aus dem Bytecode entfernt. Den Bytecode zu verkleinern ist eine Möglichkeit, den Code schneller laufen zu lassen.

Python-Debugger

Python hat einen eingebauten Debugger, pdb. Du kannst damit Code schrittweise durchgehen und Variablen untersuchen. Du kannst damit auch Breakpoints setzen. Um loszulegen, musst du zuerst import pdb ausführen und dann pdb.set_trace() an der Stelle aufrufen, an der du mit dem Debuggen beginnen möchtest:

import pdb

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

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

Wenn du diesen Code ausführst, erhältst du eine pdb-Eingabeaufforderung, in der du Befehle eingeben kannst. Gib help ein, um eine Liste der Befehle zu erhalten. Die gebräuchlichsten sind step, das in eine an dieser Zeile aufgerufene Funktion springt. next springt über einen Funktionsaufruf hinweg und geht zur nächsten Zeile. where zeigt dir, in welcher Zeile du dich befindest. Einige weitere nützliche Befehle sind whatis <variable>, das dir den Typ einer Variable anzeigt, und print(<variable>), das den Wert einer Variable ausgibt. Du kannst auch einfach <variable> verwenden, um den Wert einer Variable auszugeben. Ein weiterer Befehl ist jump <line number>, der zu einer bestimmten Zeilennummer springt.

Hier ist ein kleines Beispiel für die Verwendung des Debuggers, basierend auf dem vorherigen Code. Beachte, dass in diesem und den folgenden Beispielen Dateipfade auf MacOS- oder Linux-Plattformen Schrägstriche verwenden:

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

Breakpoints richtest du mit break <filename>:<line number> <condition> ein, wobei die Bedingung eine optionale Bedingung ist, die erfüllt sein muss, damit der Breakpoint ausgelöst wird. Du kannst einfach break schreiben, um eine Liste der von dir gesetzten Breakpoints zu erhalten. Um einen Breakpoint zu deaktivieren, kannst du disable <breakpoint number> schreiben. Um einen Breakpoint zu aktivieren, kannst du enable <breakpoint number> schreiben. Um einen Breakpoint zu löschen, kannst du clear <breakpoint number> schreiben. Um die Ausführung fortzusetzen, kannst du continue oder c schreiben. Um den Debugger zu beenden, kannst du quit oder q schreiben.

Hier ist ein Beispiel für die Verwendung der oben genannten Debugger-Befehle, basierend auf dem vorherigen Code:

>>> 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+ gibt es einen einfacheren Weg, Breakpoints zu erstellen. Wenn du einfach breakpoint() an der gewünschten Stelle schreibst, wird einer erstellt.

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