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:
- Editoren: Der du redigerer kildekoden.
- Python-konsollet: Der outputen vises, og der du kan kjøre Python-kommandoer interaktivt.
- 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?
Navigere opp og ned i call-stacken
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 returnert52
.random.randint()
har blitt kalt med argumentene(0, 52)
.random.randint()
har returnert52
.
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.