Anropsstacken

Hur vet datorn var den var?

I Lådor i RAM & datatyper bad vi dig tänka dig RAM som ett gigantiskt rutnät av lådor, var och en med sin egen adress. Varje variabel du skapar är en etikett på en sådan låda. Det modellen inte förklarade är vad som händer när ett program hoppar iväg och kör en annan del av koden, och hur det sedan vet var det ska återvända.

Ta print(). Du har skrivit det hundratals gånger. Varje gång hoppar Python iväg, kör de dussintals rader kod som faktiskt hanterar textutskrift, och återvänder till exakt nästa rad i ditt skript, inte till toppen av filen, inte till en slumpmässig plats, utan till exakt nästa rad. Hur vet Python var den ska gå tillbaka?

Svaret är en av de mest eleganta mekanismerna i datavetenskap: Anropsstacken En datastruktur i RAM som håller reda på alla aktiva funktionsanrop och deras returadresser. Växer när en funktion anropas (ny ram läggs till), krymper när den returnerar (ramen tas bort). Det är mekanismen som gör det möjligt att anropa en funktion och återvända till exakt rätt rad efteråt. Se: Anropsstacken . Det är en datastruktur i RAM som automatiskt håller reda på var varje funktion anropades ifrån och vad som hör till den. Den förklarar inte bara hur program hoppar, den förklarar varför en variabel kan existera inuti en funktion men inte utanför. Den gör scope till en fysisk konsekvens av minnet, inte ett godtyckligt påhitt.

Ramen som läggs på och plockas av

Här är det centrala: när Python anropar en funktion händer något konkret i minnet.

Python skapar en Stackram Det block i RAM som skapas för varje funktionsanrop. Innehåller funktionens parametrar, lokala variabler och returadress. Förstörs när funktionen returnerar. Se: Anropsstacken , ett reserverat block i RAM som innehåller funktionens parametrar, dess lokala variabler och, kritiskt, returadressen: adressen till raden i anroparen dit Python ska återvända när funktionen är klar. Den ramen läggs överst på anropsstacken.

Ett funktionsanrop lägger en stackram överst på anropsstacken och plockar bort den när funktionen är klarDin kodAnropsstacken1def hälsa(namn):2hej = "Hej " + namn3print(hej)45hälsa("Kalle")6print("Klart")anropreturhälsa()namn = "Kalle"hej = "Hej Kalle"returadress: rad 6huvudprogrammetväntar

Läs diagrammet från vänster. Anropet på rad 5 lägger en ny ram överst på stacken, och i den ligger både parametern namn, den lokala variabeln hej och adressen till rad 6. Huvudprogrammets ram ligger kvar under och väntar.

När hälsa() är klar plockas dess ram bort från stacken, alla dess variabler försvinner, och Python hoppar tillbaka till returadressen i ramen under, alltså rad 6. Det är svaret på frågan i lektionens rubrik: datorn vet var den var för att den lade dit en adress innan den hoppade.

Det är samma mekanism som gör att random.randint() vet var det ska gå tillbaka till, att print() vet var det ska gå tillbaka till, att varje funktion du någonsin anropar returnerar till exakt rätt ställe.

Scope, variablernas synlighet

Nu är Scope (räckvidd) Den del av koden där en variabel är synlig och tillgänglig. Lokala variabler existerar bara i den stackram de skapades i. Globala variabler existerar i huvudprogrammets ram. Scope är en direkt konsekvens av hur stackramar fungerar, inte en godtycklig begränsning. Se: Anropsstacken inte en abstrakt regel längre, det är en fysisk konsekvens av hur stacken fungerar.

Editor

Redo.

Kör koden. Den skriver ut hälsningen och kraschar sedan med NameError på sista raden.

meddelande skapades inuti hälsa()-ramen. När hälsa() returnerade förstördes ramen och alla dess variabler försvann. På nästa rad finns meddelande inte längre i minnet. Flytta in den sista print-raden i funktionen, indenterad som de andra, och kör igen: nu fungerar den, för nu läser du variabeln medan ramen fortfarande finns.

Samma namn, olika ramar

Det är fullt möjligt att ha variabler med samma namn i olika scope utan att de påverkar varandra. Varje har sin egen ram:

Editor

Redo.

Funktionen skriver ut 50, huvudprogrammet skriver ut 100. De delar ett namn men inte en adress i RAM. Det är precis som att två lådor i olika rum kan ha samma etikett, de är fortfarande separata lådor.

Byt namn på variabeln inuti funktionen till hp_i_funktionen och kör igen. Utskrifterna blir exakt desamma, och det är hela poängen: namnet spelade aldrig någon roll, ramen gjorde det.

Uppgift: Ramarna i din egen kod

Skapa filen stack_demo.py med följande:

def inner():
    x = "inuti inner"
    print(x)

def outer():
    x = "inuti outer"
    inner()
    print(x)

outer()
  1. Kör filen och läs utskriften. Vilken x skrivs ut när, och av vem? Skriv svaret som en kommentar sist i filen.

  2. Sätt en brytpunkt på print(x) inuti inner() och kör i debuggern. Hur många ramar visar Call Stack-panelen, och vad heter de?

  3. Klicka på outer-ramen i Call Stack-panelen. Vad visar Variables-panelen nu, och varför är det ett annat x än nyss?

  4. Stega ur inner() med F10 och se vad som händer med stacken. Skriv en mening om vad som försvann.

  5. Öppna din brawl/main.py och gör om experimentet i din egen kod: sätt en brytpunkt inuti en av funktionerna du skrev i förra delen och läs av Call Stack-panelen.

  6. Gå till Source Control-panelen i VS Code, stagea filen, skriv ett meningsfullt commit-meddelande och pusha.

Vad händer om

Deklarera en variabel hp inuti en av dina Brawl-funktioner och försök sedan skriva ut hp utanför funktionen efter anropet. Testa tre fall: utan någon hp utanför funktionen, med en hp utanför som har ett annat värde, och med print(hp) flyttad in i funktionen. Förklara alla tre resultaten med stackrammodellen.

Motivera & reflektera

Motivera varför det är en fördel att en lokal variabel försvinner när funktionen är klar. Vad hade blivit svårt i ett större program om alla variabler levde kvar?

Nästa nivå

Skriv en funktion som anropar en annan, sätt en brytpunkt i den inre och använd Step Into (F11) för att kliva in i den. Titta på Call Stack-panelen: hur många ramar ser du, och i vilken ordning försvinner de?