Minne og kodesporing

Å lese kode og forstå hva som vil skje om den kjøres er en helt essensiell ferdighet for en programmerer. I dette notatet skal vi forsøke å bygge en intuisjon for hva som egentlig skjer i datamaskinen når et program kjører. Å manuelt forutsi hvordan «minnet» vil endre seg steg for steg, kaller vi kodesporing.


Minne

Kildekoden til et program består av en sekvens av setninger (statements) som skal utføres én etter én i rekkefølge. Dataprogrammet som leser kildekoden og utfører setningene kalles en fortolker.

En fortolker innholder i hovedsak tre bevegelige deler som vil endre tilstand for hvert steg. Dette kaller vi for minnet til programmet:

Dersom vi vet tilstanden til de tre komponentene over, er det en mekanisk prosess å beregne hvordan tilstanden vil endre seg når neste setning utføres.

La oss se på et eksempel:

1
2
3
4
5
6
item = 'vafler'
price = 35
sold = 100
total = price * sold
result = f'Vi solgte {sold} {item} for til sammen {total} kroner'
print(result)

Programmet over består av 6 setninger, som utføres linje for linje. Vi kan følge med på hvordan tilstanden til minnet endrer seg for hver setning som utføres; klikk på neste-knappen under for å se hvordan minnet endrer seg for hvert steg.

Vi kan se en tilsvarende sekvens for alle kodeeksempler på denne nettsiden ved å klikke på «se steg» -knappene under kodeeksemplene. Der vises «neste steg» som en rød pil. Prøv det gjerne med en gang!

En variabel endrer seg

En variabel er en navngitt referanse til en verdi. Når vi tilordner en ny verdi til en variabel, endrer vi hvilket objekt variabelnavnet refererer til. Den gamle verdien kan faktisk bli liggende i minnet en stund selv uten at noen peker på den – men etter en stund vil objekter som ingen refererer til bli automatisk slettet for å frigjøre plassen til noe annet.

Vi skal benytte følgende eksempel for å illustrere hva som skjer i minnet når en variabel endrer seg. Les gjennom koden og tenk igjennom hva hensikten med koden er; kjør koden og se hva den skriver ut. Deretter kan du klikke deg gjennom steg for steg hvordan minnet endrer seg.

Tips: forsøk å forutsi hva som skjer i minnet før du klikker deg videre til neste steg.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
balance = 1000
org_balance = balance

# Regn ut renter etter ett år (vi antar 5% rente)
interest = balance * 0.05
balance += interest
print(f'Etter ett år har vi {balance} kroner på konto')

# Etter to år
interest = balance * 0.05
balance += interest
print(f'Etter to år har vi {balance} kroner på konto')

difference = balance - org_balance
print(f'Vi har fått {difference} kroner i renter etter to år')

Legg spesielt merke til forskjellen på balance-variabelen før og etter at linje 6 blir utført: det opprettes en ny verdi i minnet som balance-variabelen nå peker på. Den gamle verdien blir værende i minnet, og org_balance -variabelen refererer fremdeles til den gamle verdien.

En variabel er en navngitt referanse; men hva er egentlig en referanse? I tegningene over tegner vi referanser som piler – men egentlig er det en tallverdi som angir på hvilken posisjon i listen over objekter verdien som det referes til ligger. Vi kan tenke på en referanse som en «adresse» til en posisjon i minnet hvor det befinner seg et objekt.

En funksjon blir kalt

Når en funksjon blir kalt, opprettes det en ny funksjonsramme i minnet. En funksjonsramme er egentlig bare en ny samling med variabler som legger seg «oppå» de gamle. Når funksjonen er ferdig, blir funksjonsrammen slettet fra minnet.

1
2
3
4
5
6
7
8
def add(a, b):
    total = a + b
    return total

x = 5
z = add(x, 2)
z = add(z, 1)
print(z) # 8
Debuggeren

I VSCode (og egentlig alle gode kodeeditorer for Python) finnes det en debug-modus som tillater oss å gå gjennom vår egen kode steg for steg på lignende måte som «se steg» -knappen gjør det for oss med kodeeksemplene på denne nettsiden.

En god gjennomgang laget av Boris Paskhaver:

Manuell kodesporing med tabell

Modellen av hva som skjer i minnet som er beskrevet i avsnittene over, gir oss en god forståelse av hva som egentlig skjer. Ulempen er at det er veldig mange tegninger å lage hvis du manuelt skal spore flere steg.

En kodesporingstabell er en forenklet modell av minnet som lar oss visualisere hva som skjer over flere steg på en mer kompakt måte. Denne metoden for å spore kode er som regel god nok i praksis; i hvert fall så lenge vi har den mer presise modellen i bakhodet.

Når vi sporer koden med en tabell, oppretter vi en tabell med én kolonne for hver variabel som finnes i den delen av koden vi skal spore. Den første kolonnen kan være en tidlinje som viser hvilket linjenummer i koden som skal utføres, og den siste kolonnen kan være utskrift. Deretter fyller vi ut rad for rad i tabellen nedover. Når vi kommer til et nytt steg i koden, fyller vi ut en ny rad i tabellen.

Samme eksempel som vi har sett tidligere:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
balance = 1000
org_balance = balance

# Regn ut renter etter ett år (vi antar 5% rente)
interest = balance * 0.05
balance += interest
print(f'Etter ett år har vi {balance} kroner på konto')

# Etter to år
interest = balance * 0.05
balance += interest
print(f'Etter to år har vi {balance} kroner på konto')

difference = balance - org_balance
print(f'Vi har fått {difference} kroner i renter etter to år')
linje balance org_balance interest difference utskrift
1 1000
2 1000
5 50.0
6 1050.0
7 Etter ett år har vi 1050.0 kroner på konto
10 52.5
11 1102.5
12 Etter to år har vi 1102.5 kroner på konto
14 102.5
15 Vi har fått 102.5 kroner i renter etter to år
Manuell kodesporing av funksjoner med tabell

Et eksempel på kodesporing som inkluderer funksjonsskall:

1
2
3
4
5
6
7
8
def add(a, b):
    total = a + b
    return total

x = 15
z = add(x, 7)
z = add(z, 1)
print(z) # 8
linje x z add a add b add total add returverdi utskrift
5 15
6; 1 15 7
6; 2 22
6; 3 22
6 22
7; 1 22 1
7; 2 23
7; 3 23
7 23
8 23