Avlusing av Python-programmer i Spyder

Her følger en kort introduksjon til hvordan du kan bruke Pythons debugger i Spyder for å finne og analysere feil i programmet ditt. Eksempelprogrammet vi skal debugge er et program som trekker tilfeldige kort med tilbakelegg, og teller hvor mange unike kort du trakk.

Eksempelprogrammet

Hovedrutinen i programmet er funksjonen test_tilbakelegg(), som bruker disse hjelpefunksjonene:

  • lag_kortstokk(): Returnerer en standardkortstokk med 52 kort.
  • trekk_tilfeldig(): Returnerer ett tilfeldig kort fra angitt kortstokk, men gjør ingen antagelser om antall kort i stokken.
  • trekk_kort(): Brukes for å hente ut et bestemt kort fra kortstokken.

Programmet er oppkonstruert for å vise noen av verktøyene du har i debuggeren, og ikke et godt eksempel på hvordan man bør trekke kort fra en kortstokk. NumPy har en funksjon numpy.random.choice som kan trekke med og uten tilbakelegg, og i et virkelig program ville det vært bedre å bruke denne. Det er også kunstig at trekk_kort() er en egen funksjon, men vi skal snart se hvorfor.

Koden ser slik ut, og du kan laste ned fila ved å klikke på lenken kortstokk.py:

kortstokk.py
# -*- coding: utf-8 -*-
 
import random
 
 
def lag_kortstokk():
    """Returnerer en liste med de 52 kortene i en vanlig kortstokk."""
    farger = ["Spar", "Hjerter", "Ruter", "Kløver"]
    verdier = ["2", "3", "4", "5", "6", "7", "8", "9", "10",
               "knekt", "dame", "konge", "ess"]
    kortstokk = []
    for f in farger:
        for v in verdier:
            navn = f + v
            kortstokk.append(navn)
    return kortstokk
 
 
def trekk_tilfeldig(kortstokk):
    """Trekker et tilfeldig kort fra kortstokken."""
    n = len(kortstokk)
    i = random.randint(0, n)
    k = trekk_kort(kortstokk, i)
    return k
 
 
def trekk_kort(kortstokk, x):
    """Trekk kort nr. x fra kortstokken."""
    return kortstokk[x]
 
 
def test_tilbakelegg():
    print("Genererer kortstokk")
    kortstokk = lag_kortstokk()
 
    antall = 10
    print("Trekker", antall, "tilfeldige kort med tilbakelegg:")
    trukket = set()
    for x in range(antall):
        kort = trekk_tilfeldig(kortstokk)
        print("  Du trakk", kort)
        trukket.add(kort)
    unike = len(trukket)
    print("Trakk", unike, "ulike kort")
 
 
test_tilbakelegg()

Kjøring av programmet

Etter å ha kjørt programmet én gang vises følgende output i Python-konsollet

Genererer kortstokk
Trekker 10 tilfeldige kort med tilbakelegg:
  Du trakk Spar9
  Du trakk Ruterknekt
  Du trakk Sparess
  Du trakk Spar6
  Du trakk Kløver8
  Du trakk Kløver7
  Du trakk Ruteress
  Du trakk Sparknekt
  Du trakk Kløver4
  Du trakk Ruteress
Trakk 9 ulike kort

og alt ser ut til å fungere. Men hvis vi kjører programmet flere ganger, vil det før eller siden krasje:

Genererer kortstokk
Trekker 10 tilfeldige kort med tilbakelegg:
  Du trakk Spar4
  Du trakk Spar9
  Du trakk Hjerter5
  Du trakk Ruterdame
  Du trakk Hjerter3
  Du trakk Spar7
Traceback (most recent call last):

  File "<ipython-input-39-85f8e700afd9>", line 1, in <module>
    runfile('/Users/perhov/.spyder-py3/kortstokk.py', wdir='/Users/perhov/.spyder-py3')

  File "/Users/perhov/anaconda/lib/python3.6/site-packages/spyder/utils/site/sitecustomize.py", line 710, in runfile
    execfile(filename, namespace)

  File "/Users/perhov/anaconda/lib/python3.6/site-packages/spyder/utils/site/sitecustomize.py", line 101, in execfile
    exec(compile(f.read(), filename, 'exec'), namespace)

  File "/Users/perhov/.spyder-py3/kortstokk.py", line 47, in <module>
    test_tilbakelegg()

  File "/Users/perhov/.spyder-py3/kortstokk.py", line 40, in test_tilbakelegg
    kort = trekk_tilfeldig(kortstokk)

  File "/Users/perhov/.spyder-py3/kortstokk.py", line 23, in trekk_tilfeldig
    k = trekk_kort(kortstokk, i)

  File "/Users/perhov/.spyder-py3/kortstokk.py", line 29, in trekk_kort
    return kortstokk[x]

IndexError: list index out of range

OOPS! Programmet har klart å trekke 6 kort, og så krasjet. Hva gikk galt her?

Verktøy

I resten av forklaringen kommer vi til å referere til følgende verktøy i Spyder:

  1. Editoren: Der du redigerer kildekoden.
  2. Python-konsollet: Der outputen vises, og der du kan kjøre Python-kommandoer interaktivt.
  3. Hjelpevindu / variabelutforsker / filutforsker: Bruk knappene like under vinduet for å velge funksjon.

Starte debuggeren

Rett etter programmet krasjer, kan vi kjøre kommandoen %debug i Python-konsollet. Da ser vi at kommandolinja endres fra å vise

In [2]:

til å vise

ipdb>

Dette betyr at vi er inne i Python-debuggeren, og at kommandoene vi heretter kjører i Python-konsollet er debugger-kommandoer og ikke Python-uttrykk som skal evalueres.

Programmet er nå "spolt tilbake" til der krasjen oppstod, og hele tilstanden til programmet er tatt vare på. Vi kan derfor inspisere innholdet av variablene i programmet, og også finne ut hvor funksjonen som krasjet ble kalt fra, og hvilken input den fikk.

Debuggeren viser hvor i programmet krasjen oppstod:

> /Users/perhov/.spyder-py3/kortstokk.py(29)trekk_kort()
     27 def trekk_kort(kortstokk, x):
     28     """Trekk kort nr. x fra kortstokken."""
---> 29     return kortstokk[x]
     30 
     31 


ipdb>

Legg merke til at den samme linjen har blitt markert i editoren. Mens du debugger vil altså editoren til enhver tid vise deg hvor i koden du befinner deg:

Inspisere variabler

Hvorfor krasjet koden akkurat i trekk_kort()? Feilmeldingen

IndexError: list index out of range

tyder på at vi har prøvd å aksessere et ugyldig element av en eller annen liste.

Programmet krasjet på siste linje i trekk_kort()-funksjonen, så noe har gått feil her:

def trekk_kort(kortstokk, x):
    """Trekk kort nr. x fra kortstokken."""
    return kortstokk[x]

Vi kan se innholdet av en variabel enten ved å bruke debuggerkommandoen p <variabelnavn>, eller ved å bruke variabelutforskeren. Kommandoen p kan også (med visse begrensninger) skrive ut python-uttrykk. La oss se hvor stor kortstokken var, og hvilket kort vi ble bedt om å trekke:

ipdb> p kortstokk
['Spar2', 'Spar3', 'Spar4', 'Spar5', 'Spar6', 'Spar7', ..., 'Kløverkonge', 'Kløveress']

ipdb> p len(kortstokk)
52

ipdb> p x
52

AHA! Vi har en off-by-one-feil her. Python begynner å telle på 0, i motsetning til MATLAB og Fortran. Gyldige indekser inn i kortstokk-vektoren er fra og med 0 til og med 51.

Vi kan se det samme ved å klikke på Variable explorer-knappen:

Men hvorfor har trekk_kort() blitt kalt med 52 som argument?

Vi har slått fast at funksjonen trekk_kort() har blitt kalt med et argument som ikke gir mening, men hvordan ble argumentet regnet ut? Dette har skjedd et annet sted i koden, og for å finne ut dette må vi finne ut hvor funksjonen ble kalt fra.

Avsporing: Her begynner vi å merke nytteverdien av debuggeren. Å spore innholdet av variablene lar seg for så vidt gjøre ved å krydre programkoden med print()-uttrykk og så kjøre programmet igjen, men print()-debugging er:

  • Mer tungvint (du risikerer å måtte legge inn print()-uttrykk mange steder i koden).
  • Mer tidkrevende (du må legge inn print()-kall først, og så framprovosere feilen igjen), spesielt hvis programmet krasjer veldig sjeldent.
  • Mindre fleksibelt (du får kun inspisert de variablene du printer, og kan ikke undersøke vilkårlige variabler tilgjengelig i skopet).

For å hoppe til det stedet i koden hvor funksjonen vår ble kalt fra, bruker vi debug-kommandoen up. Trenger du å vite hvor den funksjonen ble kalt fra, kjører du up en gang til. For å navigere ned igjen i kjeden av funksjonskall, bruker du kommandoen down.

I debugger-vinduet skriver vi altså:

ipdb> up

og vi ser at debuggeren sier hvor vi nå befinner oss:

> /Users/perhov/.spyder-py3/kortstokk.py(23)trekk_tilfeldig()
     21     n = len(kortstokk)
     22     i = random.randint(0, n)
---> 23     k = trekk_kort(kortstokk, i)
     24     return k
     25 


ipdb>

Samtidig har editoren markert linjen vi ble kalt fra (1), og variabelutforskeren (2) viser at:

  • n = 52
  • i = 52

som igjen betyr at:

  • len(kortstokk) har returnert 52.
  • random.randint() har blitt kalt med argumentene (0, 52).
  • random.randint() har returnert 52.

Vi ser altså at verdien 52 kommer direkte fra random.randint(), og det er på tide å bruke hjelpefunksjonen (3):

Bruk av hjelpefunksjonen

I hjelpevinduet kan vi raskt hente opp dokumentasjonen til funksjonene vi bruker. Vi ønsker å vite hvordan random.randint brukes og hva den gjør, og skriver inn funksjonsnavnet i Object-feltet:

Her ser vi at endepunktene a og b angir et lukket intervall, mens programkoden vår har antatt at funksjonen tar et halvåpent intervall (slik som f.eks range() gjør).

Feilretting

Vi har derfor funnet ut at vi må endre hvordan vi bruker random.randint() i funksjonen trekk_tilfeldig(), og endrer linja:

i = random.randint(0, n)

til:

i = random.randint(0, n-1)

Dermed er vi i mål, og kan avslutte debuggeren med kommandoen q:

ipdb> q

Vi ser at Python-konsollet nå igjen viser:

In [3]:

som betyr at vi er tilbake til Python-kommandolinja.

2020-08-27, Per Kristian Hove