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

print('foo') # kjøres ikke
x = 1 ? 2
print('bar') # kjøres ikke

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 underveis når det kjører. Vanlige kjøretidsfeil er blant annet NameError, AttributeError, TypeError, IndexError, ZeroDivisionError og FileNotFoundError

print('foo') # kjøres
x = '42' + 1
print('bar') # kjøres ikke

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

Ordet assert har flere mulige oversettelser til norsk; men de oversettelsene som passer best i vår kontekst er «å påse» eller «å forsikre om» at noe er sant.

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.

Hvorfor krasje programmet med vilje?

  • Det er bedre å krasje enn å få feil svar.
  • Hvis programmet krasjer, er det best om det krasjer så nærmt feilen som mulig.
everything_is_ok = True
assert everything_is_ok # Her skjer det ingen ting
...
everything_is_ok = False
assert everything_is_ok # Krasjer!

#   File "/demo/demo.py", line 5, in <module>
#     assert everything_is_ok
# AssertionError

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

def calculate_rectangle_area(width, height):
    # Forsikrer oss om at bredden og høyden ikke er negative tall
    assert width >= 0
    assert height >= 0

    # Utfører funksjonen som ellers
    return width * height

print(calculate_rectangle_area(2, 16)) # Fungerer fint
print(calculate_rectangle_area(6, -3)) # 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.

Print

Assert-setninger kan fortelle oss at noe er feil. 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.

Når vi bruker print-setninger for feilsøkings-formål, kan det være nyttig å skrive ut verdien til alle variablene vi har. Dette kan gjøres med print(locals()), som vil skrive ut verdien til alle lokale variabler (altså variabler som er definert inne i funksjonen vi befinner oss i). Dersom du kaller print(locals()) utenfor en funksjon, vil du få en liste over alle variablene som er definert i det globale skopet – merk at dette også inkluderer en del variabler som er definert av Python selv, og som vi ikke trenger å bry oss om.

Tips: hvis du bruker mer enn én print-setning, er det lurt å inkludere inkludere informasjon om hvor i koden du befinner deg. For eksempel print('debug: line 3', locals()) vil skrive ut en linje som begynner med «debug: line 3» og deretter skriver ut alle lokale variabler.

Dette programmet rammer inn tre linjer med tekst og skriver det ut til skjermen.

# DENNE KODEN HAR EN BUG (MED VILJE)

def frame_border(length):
    result = '*' * length
    return result

def frame_line(text, length):
    extra_spaces = length - len(text) - 4
    center_string = text + ' ' * extra_spaces
    return '* ' + center_string + ' *'

def frame_text(line1, line2, line3):
    longest_word = max(line1, line2, line3)
    length = len(longest_word) + 4

    result = (
        frame_border(length) + '\n' +
        frame_line(line1, length) + '\n' +
        frame_line(line2, length) + '\n' +
        frame_line(line3, length) + '\n' +
        frame_border(length)
    )
    return result

result = frame_text('Foo', 'Hello', 'Chilibom')
print(result)

Vi kjører kode over og ser at vi får følgende output (som inneholder en logisk feil):

*********
* Foo   *
* Hello *
* Chilibom *
*********

Vi ser at rammen ikke er bred nok til å omslutte det lengste ordet, som er «Chilibom.» Kan det være at noe er feil med hvordan length -variabelen beregnes? Vi setter inn en velplassert print-setning like etter (eller før) lengden på linjen er beregent, slik at vi ser hvilke verdier som inngikk i å beregne den.

# DENNE KODEN HAR EN BUG (MED VILJE)
# Eksempel på bruk av print-setninger for å lete etter feilen

def frame_border(length):
    result = '*' * length
    return result

def frame_line(text, length):
    extra_spaces = length - len(text) - 4
    center_string = text + ' ' * extra_spaces
    return '* ' + center_string + ' *'

def frame_text(line1, line2, line3):
    longest_word = max(line1, line2, line3)
    length = len(longest_word) + 4
    print('debug: frame_text line 3', locals())  # <-- VELPLASSERT PRINT

    result = (
        frame_border(length) + '\n' +
        frame_line(line1, length) + '\n' +
        frame_line(line2, length) + '\n' +
        frame_line(line3, length) + '\n' +
        frame_border(length)
    )
    return result

result = frame_text('Foo', 'Hello', 'Chilibom')
print(result)

Vi får nå denne utskriften:

debug: frame_text line 3 {'line1': 'Foo', 'line2': 'Hello', 'line3': 'Chilibom', 'longest_word': 'Hello', 'length': 9}
*********
* Foo   *
* Hello *
* Chilibom *
*********

Vi inspiserer variablene, og finner noe urovekkende: longest_word er satt til 'Hello', selv om det lengste ordet er 'Chilibom'. Det forklarer i det minste hvorfor lengden ikke ble riktig.

Det viser seg at max-funksjonen ikke gjorde som vi forventet: vi ønsket at den skulle gi oss det lengste ordet, men i stedet gav den oss det siste ordet i alfabetet. Da kan vi fikse koden ved å bruke max-funksjonen på en annen måte, for eksempel slik:

length = max(len(line1), len(line2), len(line3)) + 4

Ulempen med print-setninger er at man må fjerne dem når man er ferdig med å fikse bug’en; og hvis det blir veldig mange print-setninger, kan det være krevende å lese gjennom.

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 i en eller annen form. I større prosjekter kan man gjerne ha egne filer som kun inneholder tester for «produksjonskoden».

import other_file as M

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

I dette kurset har noen av 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.