mitmario.dev

Tracebacks über mehrere Ebenen

Python Sandbox 4 Min Lesezeit 4 BeispieleLektion 1 von 6

Sobald Funktionen andere Funktionen aufrufen, wird die rote Meldung länger. Beim ersten Mal sieht sie wirklich aus wie eine Wand. In Lektion 1.4 hatte ein Traceback eine Fundstelle, in Lektion 8.1 zwei. Die Wand ist genauso aufgebaut wie damals, und wenn du diese Ordnung kennst, liest du auch fünf Fundstellen in ein paar Sekunden.

Drei Fundstellen

Drei Ebenen tief
def durchschnitt(werte):
    return sum(werte) / len(werte)


def tageszeile(tag, werte):
    schnitt = durchschnitt(werte)
    return f"{tag}: im Schnitt {schnitt:.1f} Grad"


print(tageszeile("Montag", [12.5, 14.0, 13.5]))
print(tageszeile("Dienstag", []))
print(tageszeile("Mittwoch", [9.0, 11.6]))

Eine kleine Wetterstation rechnet für jeden Tag den Durchschnitt der Messwerte aus. Der Montag klappt. Beim Dienstag bricht das Programm ab, und der Mittwoch kommt gar nicht mehr dran.

Gelesen wird wie immer zuerst die letzte Zeile. ZeroDivisionError: division by zero heißt Division durch null. Darüber stehen drei Zeilen mit File. Von oben nach unten gelesen sind sie der Weg, den das Programm bis zum Knall genommen hat.

  • Zeile 11 in <module> ist das Programm selbst. Hier wird tageszeile für den Dienstag aufgerufen, und die Wellenlinie mit den Dächern darunter zeigt genau auf diesen Aufruf.
  • Zeile 6 in tageszeile. Hier ruft tageszeile ihrerseits durchschnitt auf.
  • Zeile 2 in durchschnitt. Hier wird geteilt, und hier hat es geknallt.

Daher kommt auch die Überschrift most recent call last. Der jüngste Aufruf steht zuletzt. Ganz oben steht, wo alles angefangen hat, und ganz unten, wo es aufgehört hat. Im Terminal ist jede der drei Zeilen ein Button, und im Editor ist von Anfang an die unterste markiert.

Wo es knallt, und wo der Fehler liegt

Die unterste Fundstelle ist der Ort, an dem Python das Problem bemerkt hat. Ob man dort auch etwas ändern sollte, ist eine andere Frage. In durchschnitt steht Summe durch Anzahl, und genau das wird gerechnet. Am Dienstag gab es aber keine Messwerte. Die Liste ist leer, und durch null Werte kann niemand teilen. Interessanter als „Was ist an Zeile 2 falsch?“ ist deshalb die Frage, wer eine leere Liste dorthin schickt und wo entschieden werden sollte, was dann passiert.

Das Symptom weggemacht
def durchschnitt(werte):
    if not werte:
        return 0
    return sum(werte) / len(werte)


def tageszeile(tag, werte):
    schnitt = durchschnitt(werte)
    return f"{tag}: im Schnitt {schnitt:.1f} Grad"


print(tageszeile("Montag", [12.5, 14.0, 13.5]))
print(tageszeile("Dienstag", []))
print(tageszeile("Mittwoch", [9.0, 11.6]))

Am nächsten liegt eine Reparatur direkt am Knall. Bei einer leeren Liste gibt durchschnitt einfach 0 zurück, das Programm läuft durch, und da steht Dienstag: im Schnitt 0.0 Grad. Das ist falsch. Und zwar leise, denn 0 Grad ist eine echte Temperatur, für die die Wetterstation sogar eine eigene Prüfung hat.

Die Fehlermeldung ist weg, der Fehler nicht. So etwas nennt man, ein Symptom zu beheben. Was man gesehen hat, ist verschwunden. Die Ursache ist noch da und bekommt jetzt eine erfundene Antwort.

Die Ursache behoben
def durchschnitt(werte):
    return sum(werte) / len(werte)


def tageszeile(tag, werte):
    if not werte:
        return f"{tag}: keine Messwerte"
    schnitt = durchschnitt(werte)
    return f"{tag}: im Schnitt {schnitt:.1f} Grad"


print(tageszeile("Montag", [12.5, 14.0, 13.5]))
print(tageszeile("Dienstag", []))
print(tageszeile("Mittwoch", [9.0, 11.6]))

Besser fällt die Entscheidung eine Ebene höher. In tageszeile entsteht der Satz für einen Tag, und dort weiß man auch, was „nichts gemessen“ für den Leser heißen soll, nämlich keine Messwerte. durchschnitt bleibt, wie es war. Es bekommt nur keine leere Liste mehr.

Wenn die Ursache gar nicht im Traceback steht

Ein Fehler von früher
def einlesen():
    werte = []
    for zeile in ["12.5", "14.0", "13.5"]:
        werte.append(zeile)
    return werte


def durchschnitt(werte):
    return sum(werte) / len(werte)


messwerte = einlesen()
print(f"{len(messwerte)} Messwerte gelesen")
print(f"Im Schnitt {durchschnitt(messwerte):.1f} Grad")

Diesmal endet es mit TypeError: unsupported operand type(s) for +: 'int' and 'str'. Mit Plus soll hier eine ganze Zahl und ein Text zusammengerechnet werden. Der Traceback nennt zwei Fundstellen, das Programm in Zeile 14 und durchschnitt in Zeile 9. Wellenlinie und Dächer stehen unter sum(werte).

Die ganze Zahl ist der Anfang der Summe. sum fängt bei 0 an und zählt den ersten Wert dazu, und der ist ein Text.

Die Ursache liegt in einlesen. Dort fehlt das float() um jede Zeile. Nur steht einlesen nirgends im Traceback, weil ein Traceback nur die Aufrufe zeigt, die im Moment des Knalls noch liefen. einlesen war da längst fertig. Die Liste aus Texten wurde zurückgegeben und ist im Programm so lange weitergewandert, bis jemand damit rechnen wollte.

Wenn du nach der Ursache suchst, reicht die Frage nach dem Wo also nicht. Genauso wichtig ist, woher der Wert kommt, mit dem hier gerechnet wird. Im Reiter Debug sieht man das sofort. Im letzten Schritt der Aufzeichnung, der Zeile in durchschnitt, steht werte mit ['12.5', '14.0', '13.5']. An den Anführungszeichen erkennst du, dass das Texte sind. Wenn du zurückblätterst, siehst du die Liste in einlesen entstehen, Runde für Runde um einen Text länger. Wie man so eine Spur Schritt für Schritt verfolgt, kommt im letzten Teil dieses Abschnitts.

Was die Aufgabe will

Das Haushaltsbuch soll für jede Kategorie sagen, was eine Ausgabe dort im Schnitt gekostet hat. Bei einer Kategorie, in der diese Woche nichts ausgegeben wurde, bricht es ab, über drei Ebenen hinweg. Gesucht ist die Stelle, an der das entschieden werden sollte. Dort soll für so eine Kategorie ehrlich keine Ausgaben herauskommen.

Zum Mitnehmen

Ein Traceback liest man von oben nach unten als Weg, vom ersten Aufruf bis zu der Stelle, an der es geknallt hat. Die Ursache sitzt oft weiter oben. Manchmal steht sie gar nicht drin, weil die Funktion, die den falschen Wert gebaut hat, da schon fertig war.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.