Göm menyn

Funktioner

Vi har redan sett exempel på några funktioner i studiematerialet, och ni har antagligen skrivit några själva i labbarna. I det här kapitlet kommer vi gräva oss djupare i funktioner, hur de definieras och vad som händer vid funktionsanrop.

Parametrisering

Exempel 1:

Den första varianten av den här sången har vi sett förut (den hårdkodade). Den kommer alltid skriva ut "beer". Säg nu att vi även vill kunna sjunga om "milk". Då kan vi så klart göra så här:

# 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")

# 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, så istället för att den alltid skriver ut "beer" skriver den ut det som man anropar funktionen med.

# Parametriserad, generell variant
def beer_song(n, bev):
    """ 
    Sings the beer 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 fungerade 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 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 se tillfällen när man kan göra detta ä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 att slösa bort på att skriva och felsöka onödig kod. Den avancerade programmeraren kan läsa mer om DRY -- Don't Repeat Yourself.

Exempel 2:

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")

Här kommer en annan variant med samma resultat. Vi kan återanvända farm_verse-funktionen även om farm_song() är omskriven.

Exempel 3:

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)

Poängen med funktioner

Varför använder vi funktioner, istället för att 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 i koden. Genom att ha bättre ordning blir koden mer läsbar, främst genom beskrivande variabelnamn och funktioner (självdokumenterande kod). Grupperar man funktioner med likartade funktionalitet blir överblickbarheten bättre (modularisering).

Vad händer vid ett funktionsanrop?

Vi använder oss av exempel 2. Vad händer egentligen när vi anropar farm_verse("cow", "moo") på första raden i farm_song?

  1. De aktuella parametrarna beräknas ("cow och "moo")
  2. De formella parametrarna (animal och noise) tilldelas dessa värden
  3. farm_verse kropp körs, med andra ord alla print-anrop
  4. Programkontrollen lämnas tillbaks till punkten efter anropet (rad nr. 10)

Parameteröverföring

Nu ska vi kolla på hur parametrarna överförs vid ett funktionsanrop.

Exempel 1

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)

Notera att 1000 skrivs ut när vi anropar test(), inte 1050 (se körexempel nedan). Detta beror på att funktionsparametern balance i funktionen add_interest är en helt annan variabel än balance i 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 istä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å.
>>> test()
1000

Parametrar för add_interest

Exempel 2

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

Parametrar forts.

Exempel 3

När vi istället arbetar med sammansatta datatyper, fungerar Python i grunden på samma sätt.

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

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

>>> test()
[1000, 2500, 400]

Inget har ändrats! När add_interest() kördes satte den om sin lokala parameter balances till en ny lista, men detta påverkade inte balances i test().

Men eftersom vi arbetar med sammansatta datatyper har vi mer möjligheter än att bara sätta om hela variabeln till ett nytt värde. Vi kan istället gå in och "peta" inuti den lista som skickades med.

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)

>>> 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.

OBS: Detta kan ofta leda till underliga effekter ifall du använder destruktiva funktioner, då listan du skickar till en funktion sedan också påverkas utanför.

Parametrar forts.

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. Deb 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() istället sätter om värdet på balances[i], 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 it 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".

Parameteröverföringsmodeller

Programmeringsspråk har olika sätt att överföra parametrar. Dessa kallas parameteröverföringsmodeller. Här är ett par av de vanligaste parameteröverföringsmodellerna:

  • Call-by-reference innebär att det som kommer in till funktionen är referenser till data, och att funktionen mycket väl kan ändra detta.
  • Call-by-value innebär att man beräknar det som skickas in till funktionen och skickar värdet.

Python använder alltid en variant av call-by-reference. För primitiva datatyper, som inte har några ingående "delar" som kan ändras, kommer detta att fungera exakt likadant som call-by-value.

Synlighet

Vi har redan märkt att en funktions parametrar och lokala variabler existerar enbart så länge funktionen körs. Mer allmänt kan man tala om en symbols synlighet eller räckvidd och menar då den del av koden där symbolen existerar och kan användas. Detta gäller både variabler och funktioner. På engelska benäms begreppet scope eller ibland (scope of) visibility. Python använder, som många andra språk, lexikalisk räckvidd (eng. lexical scoping) ibland även kallat statisk räckvidd, vilket innebär att en symbols synlighet kan avgöras enbart genom att titta på källkoden.

Synlighet

Python har fyra nivåer av synlighet, d.v.s när Python behöver veta vilket värde en symbol har finns det fyra nivåer att söka igenom. Minnesregeln för att komma ihåg dessa är LEGB.

  • L (Local) innebär att man söker igenom den aktuella funktionen, dess parametrar och lokala variabler.
  • E (Enclosing) innebär att man söker igenom funktioner som finns en eller flera nivåer utanför den aktuella funktionen.
  • G (Global) innebär att man tittar efter symbolen på toppnivå i den aktuella modulen/filen.
  • B (Built-in) innebär att man söker igenom de inbyggda symbolerna i Python.
  • Om symbolen inte återfinns på någon av nivåerna signalerar Python ett fel.

Såhär fungerar olika nivåer av synlighet:

Synlighetsnivåer

Skuggning

Den här koden kommer att skriva ut 25 om vi kör den. Vi har en lokal symbol i f som skuggar en global symbol med samma namn och gör att den inte är åtkomlig i den inre synlighetsnivån.

x = 100

def f(x):
    return x * x

print(f(5))

I en del andra språk öppnar for en ny nivå, det gör den inte i Python. Den sista print-satsen i koden nedanför kommer alltså att skriva ut "And now i is 2" eftersom 2 är det sista värdet som i har i for-loopen.

def g(a):
    i = "Really important data!"
    for i in range(a):
        print("*")
    print("And now i is", i)
>>> g(3)
*
*
*
"And now i is 2"

Tillhörande quiz

Finnes här


Sidansvarig: Peter Dalenius
Senast uppdaterad: 2021-12-03