Göm menyn

7. Parameteröverföring

Vad är det som händer inne i Python när vi anropar en funktion? Vi behöver kanske inte förstå alla detaljer, men vi behöver åtminstone förstå hur indata förs över från en funktion till en annan. Och vi ska också fundera lite över varför vi behöver funktioner överhuvudtaget.

Vi kommer att svänga oss med en hel del avancerade begrepp i det här kapitlet, men det blir inga svåra glosförhör. Den stora poängen är att du ska skaffa dig en känsla för hur det funkar i praktiken, så att koden som du skriver blir korrekt.

1. Parametrisering och poängen med funktioner

I ett tidigare kapitel tittade vi på ett kodexempel som skriver ut en tråkig sångtext.

# Hårdkodad variant för öl
def beer_song(n):
    """ Sings the beer song starting with n bottles of beer. """
    for i in range(n, 0, -1):
        print(i, "bottles of beer on the wall,", i, "bottles of beer")
        print("Take one down, pass it around,", i-1, "bottles of beer")

Antag att vi även vill kunna sjunga om mjölk. Då kan vi så klart göra så här:

# Hårdkodad variant för mjölk
def milk_song(n):
    """ Sings the milk song starting with n bottles of milk. """
    for i in range(n, 0, -1):
        print(i, "bottles of milk on the wall,", i, "bottles of milk")
        print("Take one down, pass it around,", i-1, "bottles of milk")

Men redan nu, med bara två varianter och bara fem rader per funktion, har vi slösat med både kod och vår egen tid. Dessutom kan det ju hända att vi vill kunna sjunga om andra drycker i framtiden. I nästa variant har vi istället parametriserat vilken dryck man kan dricka. I stället för att den alltid skriver ut "beer" eller "milk" så skriver den ut den dryck som man anropar funktionen med.

# Parametriserad variant för vilken sorts dryck som helst
def beverage_song(n, bev):
    """ 
    Sings the generic beverage song starting with n bottles of the 
    provided beverage.
    """
    for i in range(n, 0, -1):
        print(i, "bottles of", bev, "on the wall,", i, "bottles of", bev)
        print("Take one down, pass it around,", i-1, "bottles of", bev)

Genom att bryta ut funktionaliteter i separata funktioner och parametrisera dem på detta sätt får vi många fördelar:

  • Det blir mindre kod att skriva. Det märker vi även när vi bara har två korta funktioner.
  • Det blir mindre kod att läsa, t.ex. när vi själva vill se hur koden egentligen fungerar två veckor senare.
  • Det blir mindre kod att felsöka om någonting blev fel.
  • När man hittar ett fel behöver man bara fixa det på ett ställe och inte flera. Man riskerar inte heller att felet bara fixas på vissa ställen. Det fanns ju bara en enda implementation av sången.
  • Man slipper hitta två nästan lika implementationer och undra om det fanns en anledning till att de skilde sig åt eller om någon har glömt att uppdatera en variant av koden. Och vilken variant är egentligen korrekt?
  • Och så vidare ...

Att kunna identifiera rätt tillfälle att parametrisera en funktion är ett mycket viktigt steg mot att bli en effektiv programmerare. Ju mer vi arbetar på detta sätt redan från början, desto mindre tid kommer vi att slösa bort på att skriva och felsöka onödig kod.

Låt oss ta ytterligare ett exempel:

def farm_verse(animal, noise):
    """ Sings a verse of the Old MacDonald song """
    print("Old MacDonald had a farm, EE-I-EE-I-O")
    print(" And on that farm he had a", animal, "EE-I-EE-I-O")
    print(" With a", noise, noise, "here and a", noise, noise, "there")
    print(" Here a", noise, "there a", noise, "everywhere a", noise, noise)
    print(" Old MacDonald had a farm, EE-I-EE-I-O")

def farm_song():
    """ Sings four verses of the Old MacDonald song with different animals """
    farm_verse("cow", "moo")
    farm_verse("pig", "oink")
    farm_verse("cat", "meow")
    farm_verse("duck", "quack")

Det här är ju en rätt smart lösning. Vi har observerat att alla verser av den klassiska sången Old MacDonald Had a Farm har samma struktur. Därför har vi byggt en mer generell funktion farm_verse() som kan skriva ut texten, givet ett djur och ett läte. Att skriva ut hela sången blir nu en fråga om bara fyra anrop till farm_verse().

Men det går att göra farm_song() ännu mer effektiv. Vi kan göra en datadriven lösning där djuren och lätena ingår i en lista. Sedan gör vi en loop över den listan och för varje element ett anrop till farm_verse().

def farm_verse(animal, noise):
    """ Sings a verse of the Old MacDonald song """
    print("Old MacDonald had a farm, EE-I-EE-I-O")
    print(" And on that farm he had a", animal, "EE-I-EE-I-O")
    print(" With a", noise, noise, "here and a", noise, noise, "there")
    print(" Here a", noise, "there a", noise, "everywhere a", noise, noise)
    print(" Old MacDonald had a farm, EE-I-EE-I-O")

def farm_song():
    """ Sings four verses of the Old MacDonald song with different animals """
    for animal, noise in (("cow", "moo"), ("pig", "oink"), ("cat", "meow"), ("duck", "quack")):
        farm_verse(animal, noise)

Exakt hur den här datastrukturen fungerar kommer vi att fördjupa oss i lite senare.

Så, vad är poängen med funktioner?

Varför brukar vi dela upp programkoden i flera olika funktioner, i stället för att bara ha all programkod i en stor hög?

Genom att använda funktioner behöver vi inte upprepa programkod, vilket gör att koden blir kortare och enklare. Detta gör koden lättare att testa och underhålla. En annan fördel med att använda funktioner är att vi skapar ordning och reda i koden. Genom att ha bättre ordning blir koden mer läsbar, främst genom beskrivande variabelnamn och funktioner. Om vi väljer bra namn på saker, så blir programkoden nästan självdokumenterande. Och om vi grupperar funktioner med likartad funktionalitet blir överblickbarheten bättre (modularisering).

2. Vad händer vid ett funktionsanrop?

Titta tillbaka på den första versionen av funktionerna farm_song() och farm_verse() ovan. Funktionen farm_song() består av fyra anrop till farm_verse(). Vad händer mer exakt när vi gör det första av dessa anrop?

  1. De aktuella parametrarna beräknas, alltså det som är indata fill farm_verse(). Resultatet av det blir strängarna "cow och "moo". Det var kanske inte så avancerat, men i teorin kan vi ju ha komplicerade uttryck som ska beräknas. I det här fallet var det dock bara konstanta strängar.
  2. De formella parametrarna animal och noise tilldelas dessa värden. De formella parametrarna är alltså namnen som står i definitionen av farm_verse(). I och med detta skapas en lokal omgivning eller miljö där vi kan köra innehållet i farm_verse().
  3. Funktionskroppen, alltså de ingående satserna, i farm_verse() körs. Det handlar alltså om fem print-satser som nyttjar parametrarna animal och noise på ett kreativt sätt.
  4. Till sist lämnas programkontrollen tillbaka till punkten efter anropet. Den lokala omgivning som vi tidigare skapade, där animal hade värdet "cow", är nu borta för evigt.

Det vi har detaljstuderat här är ett exempel på parameteröverföring. Vi har zoomat in på exakt vad som händer när information flödar från en funktion till en annan, alltså i detalj hur indata överförs till funktionen. Det är viktigt att förstå hur detta fungerar. Och antagligen är parameteröverföring lite lättare att förstå om du kollar på den film som hör till det här kapitlet.

3. Fler exempel på parameteröverföring

Låt oss ta ytterligare ett exempel som illustrerar parameteröverföring. Den här gången försöker vi simulera en mycket enkel bank. I det här exemplet ska vi beräkna årsräntan för ett konto och lägga till den till saldot, förhoppningsvis.

Första versionen

def add_interest(balance, interest):
    """ Calculates the new balance based on the interest """
    new_balance = balance * (1 + interest)
    balance = new_balance

def test():
    """ Tests the add_interest() function """
    balance = 1000
    interest = 0.05
    add_interest(balance, interest)
    print(balance)

Låt oss testköra detta. Vi hoppas att det nya saldot ska vara 1050, eftersom vi borde få 5% ränta insatt på kontot.

>>> test()
1000

Nej, det funkade inte! Anledningen till detta är att parametern balance i funktionen add_interest() är en helt annan variabel än balance i funktionen test(). Man kan säga att de är som två olika papper med samma etikett.

  • I test() finns "papperet" balance som vi har skrivit värdet 1000 på.
  • Vid funktionsanropet till add_interest() skapas ett nytt papper som också heter balance. Vi skriver av värdet 1000 på det nya papperet.
  • Inuti add_interest() stryker vi det gamla värdet 1000 och skriver 1050 i stället, men detta ändrar ju inte på värdet på det andra papperet.
  • När vi returnerar från add_interest() slängs alla dess lokala papper bort. Kvar finns det ursprungliga papperet, som det fortfarande står 1000 på.

Här är en bild som illustrerar hur det ser ut mitt under körningen:

Parametrar för add_interest()

Andra versionen

Om vi vill att add_interest() ska ändra på den balance som används i test(), får vi returnera det nya värdet ifrån add_interest(). Sedan får test() själv skriva över värdet i sin egen balance med värdet vi fick tillbaka. Här kommer ett exempel på hur det går till.

def add_interest(balance, interest):
    """ Calculates the new balance based on the interest """
    new_balance = balance * (1 + interest)
    return new_balance

def test():
    """ Tests the add_interest() function """
    balance = 1000
    interest = 0.05
    balance = add_interest(balance, interest)
    print(balance)

Kör vi denna kod kan vi nu se att värdet på balance faktiskt har ändrats.

>>> test()
1050

Här är en bild som illustrerar hur det ser ut mitt under körningen:

Parametrar för add_interest()

Tredje versionen

Så, vi har just insett att vi måste returnera värdet om det ska funka. Men bara för att göra allting lite mer rörigt så behöver vi inte göra det när vi arbetare med listor (och en del andra sammasatta datatyper).

När vi överför tal från en funktion till en annan kopierar vi bara talen rakt av, men när vi överför listor överför vi referenser. Det innebär att det är samma lista som vi är inne och petar i, oavsett om vi gör det i huvudfunktionen eller i den hjälpfunktion dit vi har skickat listan.

I det här utökade exemplet jobbar vi med flera konton, lagrade i en lista.

def add_interest(balances, interest):
    """ Calculates the new balances based on the interest """
    for i in range(len(balances)):
        balances[i] = balances[i] * (1 + interest)

def test():
    """ Tests the add_interest() function """
    balances = [1000, 2500, 400]
    interest = 0.05
    add_interest(balances, interest)
    print(balances)

När vi kör det här lilla programmet får vi följande resultat:

>>> test()
[1050.0, 2625.0, 420.0]

Här har vi alltså inte tilldelat balances ett nytt värde (en helt ny lista), utan vi har gått in och ändrat enskilda element inuti den existerande listan. Den typen av ändringar kommer faktiskt tillbaka till anroparen, så att test() nu skriver ut de ändrade värdena.

Det här måste vi vara medvetna om, så att vi inte omedvetet råkar förstöra en lista som vi fått in till funktionen.

Här är en bild som illustrerar hur det ser ut mitt under körningen av den tredje versionen:

Parametrar för add_interest()

Hur gick då detta till? Varför fungerar det så här? Om vi fortsätter analogin där en variabel är ett papper där vi har skrivit upp ett värde, kan det förklaras så här.

  • En variabel innehåller egentligen inte ett värde, såsom talet 3. Den innehåller alltid en referens till värdet, som säger att "mitt värde ligger där borta". På papperet står alltså en sorts adress: "Mitt värde är lista nummer 14".

  • När test() anropar add_interest() får vi ett nytt papper, där vi skriver av adressen: "Mitt värde är (också) lista nummer 14".

  • Om add_interest() sätter om värdet på balances, alltså balances = [], ändrar vi på papperet: "Mitt värde är (numera) lista nummer 15". Resten av koden ändrar då i lista 15, vilket inte gör någon skillnad för test() som fortfarande använder lista 14.

  • Om add_interest() i stället sätter om värdet på balances[i], alltså ett enskilt element, kommer den att ändra inuti den faktiska lista nummer 14. Och det är ju precis den listan som även test() använder.

Detta har inget med namngivningen att göra. Parametern till add_interest() hade lika gärna kunnat heta foo. Eftersom den variabeln pekar ut lista nummer 14, kommer ändringar i den listan även att synas i balances i test(), som också pekar ut lista nummer 14.

Men varför fungerade det inte så här för heltalen? Jo, det gjorde det faktiskt. Det finns bara inget sätt att ändra inuti ett heltal, motsvarande det sätt vi just ändrade inuti en lista. Om vi ändrar på värdet av balance inuti add_interest() måste det alltså vara genom att "ändra på papperet", inte genom att "ändra på den struktur som papperet pekar ut".

Sammanfattning

  • Parametrisering innebär att göra en funktion mer generell genom att lägga till en eller flera parametrar så att vi kan finjustera funktionens beteende genom att välja indata.
  • De aktuella parametrarna är det som vi skickar iväg från en funktion till en annan.
  • De formella parametrarna är namnen på de variabler som står i funktionsdefinitionens huvud, alltså namnen på den indata som kommer till funktionen vid anrop.
  • När vi anropar en funktion beräknas först de aktuella parametrarna. Därefter binds värdena till de formella parametrarna i en ny omgivning, alltså en lokal miljö som bara existerar under tiden vi kör den anropade funktionen.
  • Parameteröverföring fungerar i praktiken lite olika, beroende på vilken typ av värde vi skickar iväg. Framför allt är det bra att veta att listor skickas över som referenser. Det betyder att både den anropande och den anropade funktionen delar på samma lista.

Extramaterial

Synlighet och skuggning i Python (9 min)

Läs mer om principen "Don't Repeat Yourself"

Läs mer om parameteröverföringsmodellen call-by-reference som Python använder

Quiz om funktionsanrop


Sidansvarig: Peter Dalenius
Senast uppdaterad: 2025-08-06