Feil og debugging

Man vil ofte oppleve å ha såkalte bugs i koden, som gjør at programmet vårt ikke virker. Hvis vi er heldig, krasjer programmet med en gang, slik at vi kan finne feilen ved å lese feilmeldingen. Er vi litt mindre heldig, oppdager vi at programmet ikke virker som det skal fordi vi skjønner at svarene eller oppførselen til programmet må være feil. I verste fall oppdager vi ikke feilen i det hele tatt, men lever med et program som gir oss feil data uten at vi er klar over det.

Ulike former for feil:

Strategier for å undersøke logiske feil

Strategier for å unngå feil


Syntaks-feil

Hvis programmet krasjer før det i det hele tatt har begynt, har du en syntaks-feil. I disse tilfellene gir ofte feilmeldingen en visuell indikasjon på hvor feilen ligger. Om du bruker en teksteditor laget for Python, vil den som regel gi beskjed om syntaksfeil i form av røde streker.

x = 3
if x < 5:
    print("hurra)

#   File "/demo/demo.py", line 3
#     print("hurra)
#           ^
# SyntaxError: unterminated string literal (detected at line 3)
x = 3
if x < 5:
    print "hurra")

#   File "/demo/demo.py", line 3
#     print "hurra")
#                  ^
# SyntaxError: unmatched ')'
x = 3
if x < 5:
print("hurra")

#   File "/demo/demo.py", line 3
#     print("hurra")
#     ^
# IndentationError: expected an indented block after 'if' statement on line 2
x = 3
if x < 5
    print("hurra")

#   File "/demo/demo.py", line 2
#     if x < 5
#             ^
# SyntaxError: expected ':'
x = 3
if x < 5;
    print("hurra")

#   File "/demo/demo.py", line 2
#     if x < 5;
#             ^
# SyntaxError: invalid syntax
3 = x
if x < 5:
    print("hurra")

#   File "/demo/demo.py", line 1
#     3 = x
#     ^
# SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='?

Krasj (kjøretidsfeil)

En kjøretidsfeil (engelsk: runtime error) fører til at programmet krasjer når det kjører. Vanlige kjøretidsfeil er blant annet NameError, AttributeError, TypeError, IndexError, ZeroDivisionError og FileNotFoundError

NameErrorAttributeErrorTypeErrorIndexErrorZeroDivisionErrorFileNotFoundError

NameError
color = "green"
print(colour)

#   File "/demo/demo.py", line 2, in <module>
#     print(colour)
# NameError: name 'colour' is not defined. Did you mean: 'color'?
def foo(x):
    return x*x

print(bar(2))

#   File "/demo/demo.py", line 4, in <module>
#     print(bar(2))
# NameError: name 'bar' is not defined
x = 8
if x > 100:
    msg = "Huzza"
print(msg)

#   File "/demo/demo.py", line 4, in <module>
#     print(msg)
# NameError: name 'msg' is not defined

UnboundLocalError er også en variant av NameErrror

def foo():
    y = y + 1

foo()

#   File "/demo/demo.py", line 3, in foo
#     y = y + 1
# UnboundLocalError: local variable 'y' referenced before assignment
AttributeError
s = "FOO"
print(s.convert_to_lowercase())

#   File "/demo/demo.py", line 3, in <module>
#     print(s.convert_to_lowercase())
# AttributeError: 'str' object has no attribute 'convert_to_lowercase'
TypeError
print("foo" + 2)

#   File "/demo/demo.py", line 1, in <module>
#     print("foo" + 2)
# TypeError: can only concatenate str (not "int") to str
a = ["foo", "bar"]
a["zig"] = "zag"

#   File "/demo/demo.py", line 3, in <module>
#     a["zig"] = "zag"
# TypeError: list indices must be integers or slices, not str
IndexError
a = ["foo", "bar"]
a[2] = "zag"

#   File "/demo/demo.py", line 2, in <module>
#     a[2] = "zag"
# IndexError: list assignment index out of range
s = "foo"
print(s[-4])

#   File "/demo/demo.py", line 2, in <module>
#     print(s[-4])
# IndexError: string index out of range
ZeroDivisionError
x = 5
y = 0
print(x/y)

#   File "/demo/demo.py", line 3, in <module>
#     print(x/y)
# ZeroDivisionError: division by zero
FileNotFoundError
filename = "no/such/file.txt"
with open(filename, "rt", encoding='utf-8') as f:
    content = f.read()
    print(content)

#   File "/demo/demo.py", line 2, in <module>
#     with open(filename, "rt", encoding='utf-8') as f:
# FileNotFoundError: [Errno 2] No such file or directory: 'no/such/file.txt'

Logiske feil

Logiske feil oppstår når programmet kjører, men gir oss feil svar.

x = 2
y = 3
z = x + y
print(f"{x} + {z} = {y}") # Logisk feil, vi har byttet z og y

# Gir output:
#   2 + 5 = 3
Assert

En assert er en måte for oss til å krasje programmet på egen hånd dersom vi oppdager at noe ikke er som det skal. Dette hjelper oss å finne logiske feil så tidlig som mulig.

something_is_as_it_is_supposed_to_be = True
if_this_is_false_then_something_is_wrong = False # Noe er feil!
assert(something_is_as_it_is_supposed_to_be) # Her skjer det ingen ting
assert(if_this_is_false_then_something_is_wrong) # Krasjer!

#   File "/demo/demo.py", line 4, in <module>
#     assert(if_this_is_false_then_something_is_wrong) # Krasjer!
# AssertionError

Vi kan bruke assert-setninger rundt om kring i koden for å sjekke at ting er slik vi forventer.

def sum_of_nums_between(nums, lo, hi):
    assert(lo <= hi) # Sjekker at lo faktisk er mindre enn hi
    return sum(nums[lo:hi])

print(sum_of_nums_between([0, 1, 2, 3], 0, 2)) # Fungerer fint
print(sum_of_nums_between([0, 1, 2, 3], 2, 0)) # Krasjer

Eller vi kan bruke assert-setninger for å teste at funksjoner og hjelpefunksjoner gir de svarene vi forventer

def distance(x0, y0, x1, y1):
    return ((x0 - x1)**2 + (y0 - x1)**2)**0.5

assert(5 == distance(0, 3, 4, 0)) # Ojsann, her oppdager vi at noe er feil!

Fordelen med assert-setninger (i forhold til print-setninger, se under) er at en assert er helt stille og plager ingen så lenge ting fungerer som de skal; men sier i fra med én gang noe er feil. Ulempen er at de ikke gir særlig detaljert informasjon. Vi kan imøtekomme dette noe ved å lage vår egne, forbedrede assert-funksjoner.

def assert_almost_equals(expected, actual, threshold=0.0000001):
    if not abs(expected - actual) <= threshold:
        raise AssertionError(f"Expected {expected} but actual was {actual}")

def distance(x0, y0, x1, y1):
    return ((x0 - x1)**2 + (y0 - x1)**2)**0.5

assert_almost_equals(5, distance(0, 3, 4, 0)) # Bedre feilmelding
Print

Assert-setninger kan fortelle oss at noe er feil hvis vi klarer å tenke ut på forhånd hva slags feil som kan oppstå. Men ofte er det slik at vi ikke helt vet hva som er feil, eller hvorfor det ble feil. Da ønsker vi å spore hva koden gjør; og da er det nyttig å vite hvilke verdier som faktisk befinner seg i programmet vårt. For å se dette kan vi bruke print-setninger.

# DENNE KODEN HAR EN BUG (med vilje)!!!!
# Når du kjører den, vil koden kjøre evig og aldri terminere

def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    max_factor = round(n**0.5)
    for factor in range(3, max_factor + 1, 2):
        if n % factor == 0:
            return False
    return True

def nth_prime(n):
    found = 0
    guess = 0
    while (found <= n):
        guess += 1
        if (is_prime(guess)):
            found + 1
    return guess

print('Neste linje vil henge (kjøre evig):')
print(nth_prime(5))

Vi kan feilsøke koden ved å legge inn en velplassert print-setning.

# DENNE KODEN HAR FREMDELES EN BUG (med vilje)!!!!
# Når du kjører den, vil koden kjøre evig og aldri terminere

def is_prime(n):
    if (n < 2):
        return False
    if (n == 2):
        return True
    if (n % 2 == 0):
        return False
    max_factor = round(n**0.5)
    for factor in range(3,max_factor+1,2):
        if (n % factor == 0):
            return False
    return True

def nth_prime(n):
    found = 0
    guess = 0
    while (found <= n):
        print(guess, found) ### <--- Her er print-setningen vår
        guess += 1
        if (is_prime(guess)):
            found + 1
    return guess

print('The next line will hang (run forever):')
print(nth_prime(5))

Eller enda bedre, med bruk av funksjonene locals() og input()

# DENNE KODEN HAR FREMDELES EN BUG (med vilje)!!!!
# Når du kjører den, vil koden kjøre evig og aldri terminere

def is_prime(n):
    if (n < 2):
        return False
    if (n == 2):
        return True
    if (n % 2 == 0):
        return False
    max_factor = round(n**0.5)
    for factor in range(3,max_factor+1,2):
        if (n % factor == 0):
            return False
    return True

def nth_prime(n):
    found = 0
    guess = 0
    while (found <= n):
        print(locals()) ### <--- Her er print-setningen vår
        input()         ### <--- Sett på pause til du trykker enter
        guess += 1
        if (is_prime(guess)):
            found + 1
    return guess

print('The next line will hang (run forever):')
print(nth_prime(5))

Ulempen med print-setninger er at man må fjerne dem når man er ferdig med å fikse bug’en.

Se steg med Python Tutor

Et alternativ til å bruke print-setninger for å se verdiene i programmet, er å kjøre koden i Python Tutor sitt visualiseringsverktøy. Dette vil fungere så lenge koden din befinner seg kun i én fil og ikke benytter seg av eksotiske biblioteker, leser filer, bruker nettverk eller kjører parallelle prosesser; altså er det mest aktuelt for små og enkle programmer. Til gjengjeld er visualiseringen svært god, og man kan ta steg både fremover og bakover i tid.

I verktøyet kan man kopiere inn koden sin, gå gjennom koden steg for steg, og se hvordan variablene endrer seg. I kursnotatene kan man klikke på “se steg” -knappen for å laste eksempelet automatisk inn i dette verktøyet.

VSCode sin debugger

Debug-verktøyet i VSCode (og andre gode kodeeditorer) er den profesjonelle utvikleren sitt viktigste verktøy. Det fungerer nesten som Python Tutor sitt visualiseringsverktøy, men har en del flere funksjoner, og fungerer uten de begrensningene som ligger i Python Tutor. Det eneste Python Tutor kan gjøre som ikke kan gjøres med denne debuggeren, er å gå “baklengs” i tid gjennom stegene (som riktignok er en svært hendig funksjon, og en grunn til å bruke Python Tutor hvis det ellers er egnet).

En god gjennomgang laget av Boris Paskhaver:

Test-drevet utvikling

Test-drevet utvikling er en måte å skrive kode på hvor man kontinuerlig inkluderer tester for alle delene av koden man skriver. Typisk skrives testene som assert-setninger av en eller annen form. I større prosjekter kan man gjerne ha egne filer som kun inneholder tester for “den ekte” koden.

import other_file as M

assert(5 == M.distance(0, 3, 4, 0))

I dette kurset har de fleste oppgavene i labene inkludert egne assert-setninger som tester funksjonen som skal skrives. Dette er et eksempel på test-drevet utvikling, hvor vi allerede har gjort litt av jobben for dere. Dersom det ikke er ferdigskrevne tester fra før, eller testene som finnes fra før er for dårlige, bør vi skrive våre egne tester.

Noen mener at testene alltid skal skrives før koden. Det kan ofte være en god idé; men det er også nyttig å skrive tester like etter man (tror man) er ferdig. Da kan man oppdage feil man har gjort. En av de viktigste funksjonen ved tester er dessuten å beskytte kode mot idioter fra fremtiden (gjerne oss fremtidige selv) som ønsker å endre på koden. Hvis vi har vært flinke til å utstyre koden vår med tester, vil ødeleggende endringer som gjøres i fremtiden oppdages med én gang.