Funksjoner


Funksjonskall

Å kalle en funksjon betyr at vi instruerer funksjonenen til å kjøres. For eksempel gjør vi et kall til print -funksjonen i denne kodesnutten:

a = "Hello"
b = "World"
print(a + b)
...

Når vi kaller en funksjon, gir vi ofte funksjonen noe informasjon som den trenger for å gjøre jobben sin. Denne informasjonen kalles et argument. I eksempelet over blir verdien "HelloWorld" gitt som argument til print-funksjonen (det er denne verdien uttrykket a + b evaluerer til).

Kontrollflyt ved funksjonskall
Funksjonskall med returverdi

Et annet eksempel på et funksjonskall, er kallet til len -funksjonen i kodesnutten under. Dette er en funksjon med en returverdi.

s = "Hello"
x = len(s)
...

Her blir verdien "Hello" gitt som argument til len-funksjonen. Funksjonen returnerer så verdien 5, før variabelen x endres til å peke på denne verdien.

Kontrollflyt ved funksjonskall til len

Alle funksjonskall har en returverdi. For eksempel returnerer input-funksjonen den strengen brukeren skriver inn. Vi kan lagre denne strengen i en variabel, og bruke den senere i programmet.

Eksempler på innebygde funksjoner i Python og deres returverdi.

x = max(1, 2, 3)   # x = 3
y = min(1, 2, 3)   # y = 1
z = abs(-6)        # z = 6
print(x, y, z)

s = input("Skriv noe: ") # s = "Hallo"    (hvis brukeren skriver Hallo)
l = len(s)               # l = 5          (hvis brukeren skrev Hallo)
print(s, l)

I disse eksemplene er det returverdien fra funksjonskallene som lagres med variablene x, y, z, s og l.

Print-funksjonen returnerer den spesielle verdien None. Det er helt meningsløst å ta vare på den, men det er teknisk sett mulig:

x = print("Hello") # Skriver ut Hello. Returverdien fra print lagres i x
print(x)           # None

Alle funksjoner har teknisk sett en returverdi; men for noen funksjoner er returverdien alltid den spesielle verdien None. Hvis vi litt flåsete hevder at en funksjon ikke har en returverdi, mener vi altså egentlig at returverdien alltid er None.

Funksjonskall med både returverdi og sideeffekt

Et tredje eksempel på et funksjonskall vi har sett før, er input-funksjonen. Denne funksjonen har både sideeffekt og en returverdi. Med sideeffekt mener vi at noe skjer i «verden forøvrig» som følge av funksjonskallet, som ikke er en returverdi. I dette tilfellet skjer det noe på skjermen: teksten «Navn: » vises.

name = input("Navn: ")
...
Kontrollflyt ved funksjonskall til input

Her blir strengen "Navn:" gitt som argument til input-funksjonen. Som en sideeffekt av input-funksjonen vises denne strengen i terminalen. Brukeren skriver inn f. eks. «Ole». Funksjonen returnerer så verdien "Ole" før variabelen name endres til å peke på denne verdien.

Et funksjonskall kan ha to typer effekter:

  • Returverdi
  • Sideeffekter

En returverdi er den verdi som et funksjonskall evalueres til. For eksempel evalueres funksjonskallet max(1, 2, 3) til 3. Returverdien kan lagres i en variabel, eller brukes som en del av et større uttrykk. Eksempler på funksjoner med en meningsfylt returverdi er max, min, input og len.

En sideeffekt er en endring i «verdens tilstand forøvrig» som skjer på grunn av et funksjonskall. For eksempel har print -funksjonen en sideeffekt i at det dukker opp tekst på skjermen. Alle tegne-funksjonene som er dokumentert i kursnotatene om grafikk er også eksempler på funksjoner med sideeffekter (de tegner figurerer som blir vist på skjermen).

Det er som regel sideeffekter sluttbrukeren til syvende og sist ønsker seg av et program: noe vises på skjermen, innholdet i en fil endrer seg eller lignende. Samtidig har funksjoner uten sideeffekter store fordeler: de er lettere å teste og feilsøke, og det er lettere å modularisere et program med dem.

En av de aller vanligste kildene til forvirring for ferske programmerere er forskjellen på returverdi og sideeffekt; nærmere bestemt, forskjellen mellom å returnere en verdi og å skrive ut en verdi til skjermen.

Å skrive noe ut på skjermen er en sideeffekt, det innebærer ikke å returnere noe.

Hvorfor funksjoner?

Noen har gjort et kall til print-funksjonen: under panseret i datamaskinen skjer det noe greier, og så «vipps!» dukker det opp tekst i terminalen. Heldigvis trenger ikke vi å tenke på noe av det. print-funksjonen bare fungerer. Vi kan gjenbruke noen andre sin genialitet uten at det koster oss en kalori. Dette bringer oss til de viktigste hensiktene med funksjoner: å gjenbruke kode og å abstrahere bort detaljer.

  • Gjenbruk av kode. Om vi har en samling instruksjoner vi ønsker å kjøre flere ganger, kan vi legge disse instruksjonene i en funksjon, og deretter kalle funksjonen hver gang vi ønsker å kjøre instruksjonene. Da slipper vi å skrive den samme koden flere ganger. Dette gjør det enklere å endre på koden eller rette feil senere.

  • Selvdokumenterende kode. Funksjoner har navn som ideelt sett beskriver hva funksjonen gjør. Dette gjør det enklere å lese koden.

  • Abstraksjon. Funksjoner lar oss abstrahere bort detaljer som ikke er umiddelbart relevante for det vi holder på med. Ved å dele inn koden i et hierarki av mer og mer spesialiserte funksjoner, kan vi fokusere på det som er viktig for oss på det abstraksjonsnivået vi befinner oss på.

Abstraksjon er å se bort fra detaljer som ikke er umiddelbart relevante for det vi holder på med.

I en tidlig fase av sommerferie-planleggingen sier vi gjerne til hverandre «først besøker vi bestemor, så drar vi til Sverige, og så drar vi en uke på hytten.» Vi trenger ikke å vite hvordan vi kommer oss til bestemor, eller hvordan vi kommer oss til Sverige – vi bare antar at det finnes en løsning på dette. Vi abstraherer altså bort detaljene, og fokuserer på det som er viktig for oss på dette «abstraksjonsnivået».

Når vi senere detaljplanlegger reisen til bestemor, sier vi til hverandre «først tar vi bussen til byen, så spiser vi lunsj på stasjonen, så tar vi toget videre til Hønefoss, så henter bestemor oss der». Vi er nå på et litt lavere abstraksjonsnivå enn før, men vi bryr oss fremdeles ikke om hvordan bussen eller toget fungerer – vi antar bare at bussen gjør som vi forventer, uten at vi trenger å ofre en tanke på trafikkregler eller bussmotorer. Vi abstraherer altså fremdeles bort detaljene, og fokuserer på det som er viktig for oss.

Når vi programmerer, er det lurt å skille fra hverandre instruksjoner som befinner seg på ulike abstraksjonsnivåer. Dette gjør vi blant annet ved å dele kode inn i funksjoner.

Vår første funksjon

En funksjon er en sekvens med kommandoer man kan referere til ved hjelp av et funksjonsnavn. Man kan utføre funksjonen flere ganger ved å kalle den flere ganger.

# Vi definerer en funksjon som heter `my_sample_function`
def my_sample_function():
    print("A")
    print("B")
    print("C")

# Vi kaller my_sample_function to ganger
my_sample_function()
my_sample_function()

Funksjonskroppen (setningene som skal utføres når funksjonen kjører) må ha riktig innrykk. God stil tilsier at innrykket består av 4 mellomrom.

def hello():
    print("Skriv ditt navn:")
    name = input()
   print(f"Hei {name}") # Krasjer, mangler et mellomrom

hello()
Definere egne funksjoner

For å definere vår egen funksjon:

Funksjonskroppen er instruksjonene som skal utføres når funksjonen kalles, og må ha et innrykk i forhold til def-ordet. Standard innrykk er 4 mellomrom.

# Vi definerer en funksjon som heter «my_sample_function»
def my_sample_function(my_crazy_parameter, my_insane_parameter):
    print("*********")
    print("*", my_crazy_parameter)
    print("*", my_insane_parameter)
    print("*********")

# Vi kaller my_sample_function 
my_sample_function("foo", "bar")
my_sample_function("baz", "qux")

Parameter. En funksjon har som regel noen ukjente variabler den trenger for å gjøre jobben sin. De ukjente variablene kalles parametre til funksjonen. Den som definerer en funksjon bestemmer selv hva parameterne heter. Når funksjonen kalles må parametrene til funksjonen fylles med argumenter.

Det er fort gjort å blande sammen begrepene argument og parameter, da de på en måte beskriver to sider av samme sak; men tenk på det slik:

  • Et argument er en en verdi som kun eksisterer når en funksjon blir kalt.
  • En parameter er en variabel, altså en navngitt referanse til et argument.

På samme måte som en variabel kan eksistere i kildekoden vi skriver selv om programmet ikke kjører akkurat nå, kan en parameter eksistere i funksjonsdefinisjonen selv om funksjonen ikke kjøres akkurat nå. Argumenter eksisterer egentlig bare når funksjonen faktisk kalles.

En funksjon må være definert før den kalles

find_age() # Krasjer, funksjonen find_age er ikke definert enda

def find_age():
    print("Hvilket år ble du født?")
    birth_year = int(input())
    age = 2023 - birth_year
    print(f"Du blir {age} år i år")

En setning som befinner seg inne i en funksjonskropp kan derimot kalle andre funksjoner som defineres senere i koden; så lenge den andre funksjonen er definert når den kalles er det tilstrekkelig.

def besok_bestemor():
    reis_til_bestemor()
    print("Spis kake")
    reis_hjem_fra_bestemor()

def reis_til_bestemor():
    print("Ta bussen til byen")
    print("Ta toget til Hønefoss")
    print("Bli hentet av bestemor på stasjonen")

def reis_hjem_fra_bestemor():
    print("Bli kjørt til Hønefoss av bestemor")
    print("Ta toget til byen")
    print("Ta bussen hjem")

besok_bestemor()

Hva skjer hvis du gjør kallet til besok_bestemor() like før du skriver definisjonen av reis_hjem_fra bestemor i stedet for etterpå? Krasjer det? Hvis ja; når og hvorfor krasjer det?

Retursetninger

Dersom en funksjon skal returnere en verdi, må den ha en retursetning. Returverdien er den verdien uttrykket i retur-setningen evaluerer til.

def square(x):
    return x * x

x2 = square(3)
print(x2) # 9

Dersom en funksjon ikke har noen retursetning, eller retur-setningen ikke inneholder et uttrykk, returnerer funksjonen den spesielle verdien None.

def a():
    print("Denne funksjonen returnerer None")
    return None

return_value_a = a()
print(return_value_a) # None

def b():
    print("Denne funksjonen har en tom return-setning")
    return
    
return_value_b = b()
print(return_value_b) # None

def c():
    print("Denne funksjonenen har ikke return-setning")

return_value_c = c()
print(return_value_c) # None

Det er ikke nødvendig at en retursetning er den siste setningen i en funksjon; men når en retursetning utføres, avsluttes funksjonen umiddelbart uten å fortsette videre i funksjonskroppen. Hvis man har kode etter en retursetning, kalles dette gjerne død kode (og det er selvfølgelig svært dårlig stil).

def hello():
    print("Hello")
    return
    print("Goodbye") # død kode; utføres aldri

hello() # Hello

Det kan ofte være nyttig å ha en tidlig retur-setninger inne i en if-setning, for å avslutte funksjonen tidlig under gitte betingelser.

def go_to_club(name, age):
    if age < 18:
        return f"Yo {name}, you're too young!"
    result = f"Hi there, {name}! It's time to party"
    result += "party"
    result += "party"
    result += "party"
    result += "party"
    return result + "!"

print(go_to_club("Ole", 14)) # Yo Ole, you're too young!
print(go_to_club("Kari", 18)) # Hi there, Kari! It's time to partypartypa...
Vanlig feil: forveksle returverdi og sideeffekt

En av de vanligste feilene ferske programmerere gjør, er å forveksle returverdi og sideeffekt. Dette er en feil som er lett å gjøre, fordi det er lett å tenke at en funksjon som skriver ut noe på skjermen, returnerer det den skriver ut. Dette er ikke tilfelle.

def cubed(x):
    print(x**3)   # Funksjon uten retur-verdi, kun side-effekt

cubed(2)          # ser ut til å virke
print(cubed(3))   # rart (skriver også ut `None`)
print(2*cubed(4)) # Krasj!

Gjør det heller slik:

def cubed(x):
    return x**3   # Funksjonen har retur-verdi, men ingen side-effekt

cubed(2)          # ser ikke ut til å virke (hvorfor?)
print(cubed(3))   # funker!
print(2*cubed(4)) # funker!
Skop

En variabel eksisterer i ett skop basert på hvor variabelen ble definert. Hver funksjon har sitt eget skop; variabler som er definert i dette skopet kan ikke nås utenfra.

def foo(x):
    print(x)

foo(2) # skriver ut 2
print(x) # Krasjer, siden variabelen x kun var definert i foo sitt skop
def bar():
    y = 42
    print(y)

bar() # skriver ut 42
print(y) # Krasjer, siden variabelen y kun var definert i bar sitt skop

Det samme variabelnavnet kan eksistere i ulike skop. Men selv om variablene heter det samme, er de helt uavhengig av hverandre.

def f(x):
    print("Vi er i f, x =", x)
    x += 5
    return x

def g(x):
    y = f(x*2)
    print("Vi er i g, x =", x)
    z = f(x*3)
    print("Vi er i g, x =", x)
    return y + z

print(g(2))

Det kan eksistere flere skop samtidig når koden kjører.

x = "x i globalt skop"
y = "y i globalt skop"

def f():
    y = "y i lokalt skop"
    z = "z i lokalt skop"
    print(x)
    print(y)
    print(z)

f()

Hold tungen rett i munnen og regn ut hva svaret blir før du kjører koden under. Ta notater på papir for å holde styr på hva som foregår.

def f(x):
    print("Vi er i f, x =", x)
    x += 7
    return round(x / 3)

def g(x):
    x *= 10
    return 2 * f(x)

def h(x):
    x += 3
    return f(x+4) + g(x)

print(h(f(1)))