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. . 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. , 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.
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. inte en abstrakt regel längre, det är en fysisk konsekvens av hur stacken fungerar.
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:
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() Kör filen och läs utskriften. Vilken
xskrivs ut när, och av vem? Skriv svaret som en kommentar sist i filen.Sätt en brytpunkt på
print(x)inutiinner()och kör i debuggern. Hur många ramar visar Call Stack-panelen, och vad heter de?Klicka på
outer-ramen i Call Stack-panelen. Vad visar Variables-panelen nu, och varför är det ett annatxän nyss?Stega ur
inner()med F10 och se vad som händer med stacken. Skriv en mening om vad som försvann.Öppna din
brawl/main.pyoch 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.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?