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.

def sum_of_squares(a, b):
    square_of_a = a * a
    square_of_b = b * b
    return square_of_a + square_of_b

x = sum_of_squares(1, 1)
print(x) # 2

y = sum_of_squares(1, x)
print(y) # 5

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

sum_of_squares(1, 2) # Krasjer, funksjonen sum_of_squares er ikke definert enda

def sum_of_squares(a, b):
    square_of_a = a * a
    square_of_b = b * b
    return square_of_a + square_of_b

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 (som ikke er None), 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) # også None

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

return_value_c = c()
print(return_value_c) # også 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!
Mer enn én returverdi

Det er teknisk sett kun mulig å ha én returverdi i en funksjon. Men: det er mulig at returverdien er en «tuple» (en slags liste). Da kan vi for alle praktiske formål tenke på det som om funksjonen returnerer flere verdier.

def plus_minus(x, epsilon):
    x_lo = x - epsilon
    x_hi = x + epsilon
    return x_lo, x_hi # komma for å skille verdier lager en «tuple»

# Man får egentlig kun én returverdi, men den kan pakkes opp til flere
window = plus_minus(1000, 5)
a = window[0]
b = window[1]
print(a, b) # 995 1005

# Python har en snarvei for å pakke opp en tuple med én gang
a, b = plus_minus(50, 2)
print(a, b) # 48 52 

Man kan ha så mange elementer man vil i en tuple (men det må matche antall variabler når vi pakker opp).

def three_next_numbers(n):
    return n + 1, n + 2, n + 3

a, b, c = three_next_numbers(10)
print(a, b, c) # 11 12 13

a, b = three_next_numbers(10) # Krasjer: returnert tuple har 3 verdier, ikke bare 2
print(a, b)
Ordbok
Begreper som omhandler funksjoner

1
2
3
4
5
6
7
8
9
def sum_of_squares(a, b):
    square_of_a = a * a
    square_of_b = b * b
    return square_of_a + square_of_b

x = sum_of_squares(3, 4) + 42


print(x)

Funksjonsdefinisjon. En funksjonsdefinisjon forteller Python at vi ønsker å definere en ny funksjon. En funksjonsdefinisjon består av følgende komponenter:

I eksempelet vist over utgjør linje 1-4 en funksjonsdefinisjon.

At en funksjon defineres, betyr ikke at funksjonskroppen kjøres. Det er bare når funksjonenen kalles at funksjonskroppen kjøres.

Funksjonskall. Et funksjonskall er en instruksjon om å kjøre en funksjon. Et funksjonskall består av følgende komponenter:

I eksempelet vis over utgjør sum_of_squares(3, 4) på linje 6 et funksjonskall.

Alle funksjonskall er uttrykk (men ikke alle uttrykk er funksjonskall). Et funksjonskall evalueres til returverdien fra funksjonen som kalles. I eksempelet over evalueres funksjonskallet sum_of_squares(3, 4) på linje 6 til verdien 25.

Parameter. En parameter er en lokal variabel. Parametre får angitt hvilke verdier de refererer til når funksjonen kalles, og slutter å eksistere når funksjonskallet er ferdig/returnerer. I eksempelet over er a og b parametre til funksjonen sum_of_squares, og verdiene de blir angitt å referere til når funksjonen kalles er 3 og 4.

Argument. Et argument er en verdi som gis en funksjon når den kalles. I eksempelet over er 3 og 4 argumenter ved funksjonskallet til sum_of_squares på linje 6. Når en funksjon kalles, vil parametrene (i utgangspunktet) referere til argumentene.

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, f. eks. 42 eller "Ole".
  • En parameter er en variabel, altså en navngitt referanse, f. eks. age eller name.

Et argument må eksistere før en funksjon kalles, mens en parameter er en variabel som opprettes når funksjonen kalles. Så vil parameteren referere til argumentet.

  • En parameter kan potensielt endre hvilket objekt den referer til underveis i funksjonskallet; men hvis parameteren endres slik, så slutter den altså å referere til argumentet (det er da ikke argumentet som endrer seg, kun parameteren).

Lokal variabel. En lokal variabel er en variabel som er definert inne i en funksjonskropp. Lokale variabler eksisterer kun underveis i et funksjonskall, og slutter å eksistere når kallet er ferdig/returnerer. I eksempelet over er square_of_a og square_of_b i tillegg til parametrene a og b lokale variabler i funksjonen sum_of_squares.

Global variabel. En global variabel er en variabel som er definert utenfor en funksjonskropp. Slike variabler slettes ikke fra funksjonskall til funksjonskall, men eksisterer så lenge programmet kjører. I eksempelet over er x en global variabel.

Signatur. En funksjonssignatur består minimum av funksjonsnavnet og en opplisting av parametrene til funksjonen. Funksjonssignaturen i eksempelet over er altså «sum_of_squares(a, b)». En signatur er den informasjonen som er nødvendig å kjenne til for å kunne kalle funksjonen. I tillegg til funksjonsnavn og parametre, kan også informasjon om returverdien (f. eks. hvilke type den har) og docstring-kommentarer inngå i signaturen.

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