Kapittel 6 Funksjoner


Med løkker, lister, og betingede setninger under beltet ditt skal det ikke mye til for å skrive mange linjer med kode. Selv om dette er i hovedsak en god ting begynner vi å støte på et problem, nemlig repeterende kode. I dette kapittelet skal du forstå problemene som repetisjon fører med seg. Vi skal også se at å skrive våre egne funksjoner kan hjelpe veldig med å unngå repetisjon. Du vil lære om parametere, argumenter, returverdier, og dokumentasjon av funksjoner. I prosjektdelen til slutt skal vi fullføre handlelisteassistenten som vi startet på i prosjektdelen til kapittel 4.

6.1 Introduksjon - Hvordan håndtere repetisjon av kode?

Du skal skrive kode som kobler sammen norske katteelskere i en SMS-gruppe. I denne gruppen skal søte kattebilder bli delt uten hemninger. I første omgang trenger du å verifisere at telefonnumrene til personene i gruppen er gyldige før du setter dem sammen. Dette har vi allerede skrevet kode for i kapittel 5 som jeg gjenbruker med små endringer her:

# Sjekker at originale medlemmer i gruppen har gyldige telefonnummer
medlemmer = ['+47 95483256', '+46 14372216', '+47 468503578', '+47 63831148']
for medlem in medlemmer:
    if len(medlem) != 12:
        print(f'Nummeret {medlem} har ikke riktig lengde.')
        continue
    elif medlem[:3] != '+47':
        print(f'Nummeret {medlem} har ikke landskode +47.')
        continue
    elif medlem[4] not in ['4', '9']:
        print(f'Nummeret {medlem} begynner ikke på 4 eller 9.')
        continue
    print(f'Nummeret {medlem} er et gyldig mobilnummer.')

Så langt, så bra! Men søte kattebilder er populære, så det kommer raskt et nytt medlem. Du må sjekke om det nye medlemmets telefonnummer er gyldig, så du skriver mye av koden på nytt:

# Sjekker at originale medlemmer i gruppen har gyldige telefonnummer
medlemmer = ['+47 95483256', '+46 14372216', '+47 468503578', '+47 63831148']
for medlem in medlemmer:
    if len(medlem) != 12:
        print(f'Nummeret {medlem} har ikke riktig lengde.')
        continue
    elif medlem[:3] != '+47':
        print(f'Nummeret {medlem} har ikke landskode +47.')
        continue
    elif medlem[4] not in ['4', '9']:
        print(f'Nummeret {medlem} begynner ikke på 4 eller 9.')
        continue
    print(f'Nummeret {medlem} er et gyldig mobilnummer.')

# Sjekker at et nytt medlem har gyldig telefonnummer
nytt_medlem = '+47 44927945'
if len(nytt_medlem) != 12:
    print(f'Nummeret {nytt_medlem} har ikke riktig lengde.')
elif nytt_medlem[:3] != '+47':
    print(f'Nummeret {nytt_medlem} har ikke landskode +47.')
elif nytt_medlem[4] not in ['4', '9']:
    print(f'Nummeret {nytt_medlem} begynner ikke på 4 eller 9.')
else:
    print(f'Nummeret {nytt_medlem} er et gyldig mobilnummer')

Vi trenger ikke å bruke en for-løkke eller nøkkelordet continue for å sjekke et enkelt nytt medlem. Likevel er det veldig mye repetisjon fra tidligere. Utfordringene blir:

  • Programmene våre blir lengre enn nødvendig. Lengre programmer er vanskeligere å holde oversikt over.

  • Programmene våre blir vanskeligere å teste. Selv om vi er sikre på at den originale sjekkingen av medlemmene i gruppen er riktig, kan det være noe feil neste gang jeg gjør det. Kanskje jeg kopierte noe feil, eller glemte å bytte ut et variabelnavn fra medlem til nytt_medlem et sted? Her må vi rett og slett teste at begge deler av programmet er riktig. Dette er unødvendig ekstraarbeid.

  • Programmene våre blir vanskeligere å utvide. Per nå sjekker vi lengden til telefonnummeret, at telefonnummeret har en norsk landskode, og at det er et mobilnummer. Sett at vi også ønsker å gjøre et oppslag hos 1881 eller en lignende tjeneste for å verifisere navnet til brukeren. Siden vi har repetert koden flere steder må vi skrive denne endringen inn flere steder. Glemmer vi et sted har vi introdusert nye feil.

Når vi gjentar kode som dette er dette et tydelig tegn på at vi bør skrive en funksjon. I dette kapittelet skal du lære hvordan du kan skrive dine egne funksjoner. Løsningen på problemet vårt over kan se slik ut:

def sjekk_telefonnummer(nummer):
    """Denne funksjonen sjekker om et telefonnummer er gyldig."""
    if len(nummer) != 12:
        print(f'Nummeret {nummer} har ikke riktig lengde.')
        return False
    if nummer[:3] != '+47':
        print(f'Nummeret {nummer} har ikke landskode +47.')
        return False
    if nummer[4] not in ['4', '9']:
        print(f'Nummeret {nummer} begynner ikke på 4 eller 9.')
        return False
    return True


medlemmer = ['+47 95483256', '+46 14372216', '+47 468503578', '+47 63831148']
for medlem in medlemmer:
    if sjekk_telefonnummer(medlem):
        print(f'Nummeret {medlem} er et gyldig mobilnummer')

nytt_medlem = '+47 44927945'
if sjekk_telefonnummer(nytt_medlem):
    print(f'Nummeret {nytt_medlem} er et gyldig mobilnummer')

Du trenger ikke fokusere så mye på detaljene i funksjonen sjekk_telefonnummer(). Merk heller at koden for å sjekke telefonnummer bare blir skrevet ned et sted, men brukt to steder. Vi bruker sjekk_telefonnummer() for å både sjekke originale og nye medlemmer. Dette gjør koden vår kortere, enklere å teste, og enklere å utvide. Nå kan katteelskerne få en trygg SMS-gruppe der de kan dele sine beste katteøyeblikk.

6.2 Definere egne funksjoner

Vi begynner med å lage en enkel funksjon for å lære hvordan funksjoner skrives. Vi skal skrive en funksjon som heter utregnet_rabatt() som skal ta inn prisen til en vare og mengden rabatt i prosent som skal bli gitt. Funksjonen skal returnere den nye prisen for varen der rabatten er medregnet. Koden blir som følger:

def utregnet_rabatt(grunnpris, rabatt):
    ny_pris = grunnpris * (100 - rabatt) / 100
    print(f'Den nye prisen er {ny_pris} kroner.')
    return ny_pris

Koden over bruker nøkkelordet def til å definere en funksjon. Funksjonen heter utregnet_rabatt() og den har grunnpris og rabatt som parameterne56. Koden innad i funksjonen er på sitt eget horisontale nivå og kalles ofte funksjonskroppen57 til funksjonen. Funksjonskroppen regner ut den nye prisen til varen i variabelen ny_pris. Dette blir både skrevet ut til terminalen og returnert med nøkkelordet return.

Dersom du kjører koden over vil du se at ingenting blir skrevet ut til terminalen. Hvorfor virker ikke funksjonen print() innad i funksjonen utregnet_rabatt()? Når du definerer en funksjon slik som over så blir ikke koden utført. Det er først når vi bruker funksjonen utregnet_rabatt() at koden i funksjonskroppen blir utført. La oss bruke funksjonen på en 1000-kroners jakke med 25 % rabatt:

def utregnet_rabatt(grunnpris, rabatt):
    ny_pris = grunnpris * (100 - rabatt) / 100
    print(f'Den nye prisen er {ny_pris} kroner.')
    return ny_pris


rabatt_på_jakke = utregnet_rabatt(1000, 25)

Kjører du koden nå vil alle linjene i funksjonskroppen til utregnet_rabatt() bli utført. Så nå blir jakken sin nye pris skrevet ut til terminalen. Den siste linjen i funksjonskroppen return ny_pris sørger for at funksjonen utregnet_rabatt() returnerer verdien ny_pris. Så variabelen rabatt_på_jakke vil få verdien som ligger i ny_pris, nemlig flyttallet 750.0.

Det fine med å ha laget en funksjon er at vi kan bruke den mange ganger. Her bruker vi utregnet_rabatt() tre ganger på tre forskjellige varer:

def utregnet_rabatt(grunnpris, rabatt):
    ny_pris = grunnpris * (100 - rabatt) / 100
    print(f'Den nye prisen er {ny_pris} kroner.')
    return ny_pris


rabatt_på_jakke = utregnet_rabatt(1000, 25)
rabatt_på_bukse = utregnet_rabatt(700, 30)
rabatt_på_sokker = utregnet_rabatt(150, 50)

Når vi skriver utregnet_rabatt(1000, 25) så sier vi at vi kaller funksjonen utregnet_rabatt() med 1000 og 25 som argumenter58. Da blir 750.0 returverdien59 til funksjonen utregnet_rabatt() med argumentene 1000 og 25. Så i koden over kaller vi funksjonen utregnet_rabatt() tre ganger med forskjellige argumenter, og får 3 forskjellige returverdier.

Vi har nå gitt et fugleperspektiv på hvordan skrive og kalle funksjoner. Likevel er det mye mer som kan sies om parametere, argumenter, og returverdier. Det er både triks og gode praksiser når man skriver funksjoner som det er greit å bli kjent med. Resten av kapittelet skal gå gjennom dette, slik at du får god tid til å øve på å skrive funksjoner.

6.3 Parametere og argumenter

La oss nå se nærmere på hvordan vi setter verdier inn i funksjoner. Først av alt kan det være greit å klargjøre hva forskjellen er på argumenter og parametere. Sett at vi har følgende funksjon:

def lag_fullt_navn(fornavn, etternavn):
    fullt_navn = f'{fornavn} {etternavn}'
    return fullt_navn


fullt_navn = lag_fullt_navn('Erna', 'Solberg')
print(fullt_navn)

Funksjonen lag_fullt_navn() er ganske rett fram. Den tar inn et fornavn og etternavn, og setter dem sammen til et fullstendig navn som den returnerer. Vi tester funksjonen ved å sette inn 'Erna' og 'Solberg' som verdier. Ikke overraskende så blir det skrevet ut 'Erna Solberg'.

Vi kaller fornavn og etternavn i funksjonsdefinisjonen parametere. Verdiene 'Erna' og 'Solberg' som vi setter inn når vi kaller funksjonen er argumenter. Det er ganske vanlig å slurve litt med denne distinksjonen. Du vil sikkert høre flere bruke begrepet argumenter når de egentlig mener parametere og vice versa. Stå gjerne frem som en god rollemodell ved å skille mellom dem.

6.3.1 Posisjonsbaserte og nøkkelordbaserte argumenter

Når du skriver lag_fullt_navn('Erna', 'Solberg'), så lener du deg på rekkefølgen til de to argumentene. Det vil si at du bevisst skriver inn argumentet 'Erna' før du skriver inn argumentet 'Solberg'. Dette kalles for posisjonsbaserte argumenter60. Dersom du bytter om posisjonen på argumentene 'Erna' og 'Solberg' så vil det bli skrevet ut 'Solberg Erna'.

Et annet alternativ er at du heller er veldig klar og skriver at argumentet 'Erna' skal bli sendt til parameteren fornavn, mens argumentet 'Solberg' skal bli sendt til parameteren etternavn. Det kan du gjøre slik:

def lag_fullt_navn(fornavn, etternavn):
    fullt_navn = f'{fornavn} {etternavn}'
    return fullt_navn


fullt_navn = lag_fullt_navn(fornavn='Erna', etternavn='Solberg')
print(fullt_navn)

Dette kalles for nøkkelordbaserte argumenter61. Her spiller det ingen rolle om vi bytter om rekkefølgen:

def lag_fullt_navn(fornavn, etternavn):
    fullt_navn = f'{fornavn} {etternavn}'
    return fullt_navn


fullt_navn = lag_fullt_navn(etternavn='Solberg', fornavn='Erna')
print(fullt_navn)

En liten kommentar om kodestil fra PEP 8: For nøkkelordbaserte argumenter så skal det ikke være et mellomrom på hver side av likhetstegnet =. Det samme vil gjelde for standardverdier som vi skal se på snart.

Ved første øyekast kan det virke som nøkkelordbaserte argumenter bare er bedre enn posisjonsbaserte argumenter. Her slipper du å bekymre deg for rekkefølgen til parameterne i funksjonsdefinisjonen. Samtidig gjør kode som etternavn='Solberg' det veldig klart at argumentet 'Solberg' blir sendt til parameteren etternavn. Så er nøkkelordbaserte argumenter alltid best?

Ikke nødvendigvis! I noen funksjoner er ikke rekkefølgen på argumentene viktig, så da introduserer nøkkelordbaserte argumenter bare mer unødvendig notasjon. Se på følgende eksempel:

def sum_av_tre_tall(tall1, tall2, tall3):
    summen = tall1 + tall2 + tall3
    return summen


første_sum = sum_av_tre_tall(5, 7, 2)
andre_sum = sum_av_tre_tall(tall1=5, tall2=7, tall3=2)

I funksjonen sum_av_tre_tall() er det ikke viktig hvilken rekkefølge du spesifiserer de tre argumentene når du bruker posisjonsbaserte argumenter. Summen blir uansett 14 når du setter inn tallene 5, 7, og 2. Her introduserer nøkkelordbaserte argumenter bare støy. I de fleste tilfeller gjør likevel nøkkelordbaserte argumenter koden klarere, og jeg anbefaler deg å bruke dem ganske flittig i din egen kode.

6.3.2 Standardverdier

I eksempelet med funksjonen lag_fullt_navn() ovenfor så møter vi på et interessant problem. Noen personer har også et mellomnavn, slik som Jonas Gahr Støre. Hvordan skal vi utvide for at også han skal kunne bli behandlet av funksjonen lag_fullt_navn()? En god tanke er å inkludere et mellomnavn som en parameter og deretter sjekke om dette er en tom streng:

def lag_fullt_navn(fornavn, mellomnavn, etternavn):
    if mellomnavn:
        fullt_navn = f'{fornavn} {mellomnavn} {etternavn}'
    else:
        fullt_navn = f'{fornavn} {etternavn}'
    return fullt_navn


navn1 = lag_fullt_navn(fornavn='Erna', mellomnavn='', etternavn='Solberg')
navn2 = lag_fullt_navn(fornavn='Jonas', mellomnavn='Gahr', etternavn='Støre')

Dette fikser problemet. Det eneste som er litt irriterende er at vi måtte endre den originale måten vi brukte funksjonen lag_fullt_navn() på navnet Erna Solberg. Nå må vi også legge på argumentet mellomnavn='' for dem uten mellomnavn. Er det ikke en bedre måte å gjøre dette på?

Det er det jaggu meg! Når du skriver funksjonsdefinisjoner er det mulig å sette standardverdier62 for enkelte parametere. Dette er verdier som vil bli brukt dersom ingen verdi er spesifisert når funksjonen blir utført. Vi kan sette parameteren mellomnavn til å ha standardverdien til en tom streng slik:

def lag_fullt_navn(fornavn, etternavn, mellomnavn=''):
    if mellomnavn:
        fullt_navn = f'{fornavn} {mellomnavn} {etternavn}'
    else:
        fullt_navn = f'{fornavn} {etternavn}'
    return fullt_navn


navn1 = lag_fullt_navn(fornavn='Erna', etternavn='Solberg')
navn2 = lag_fullt_navn(fornavn='Jonas', mellomnavn='Gahr', etternavn='Støre')

Vi har nå flyttet mellomnavn til å være den tredje parameteren. Hvorfor det? I Python må parametere som har standardverdier komme etter alle parametere som ikke har standardverdier. Dette er noe Python krever slik at det ikke vil bli misforståelser med hva som menes når vi kaller en funksjon. Vi kan fortsatt ha mellomnavn som det andre argumentet når vi kaller funksjonen lag_fullt_navn() for å opprette navn2 siden vi bruker nøkkelordbaserte argumenter.

Merk at i koden over så trenger vi ikke endre på den originale måten vi brukte funksjonen lag_fullt_navn() på navnet Erna Solberg. I det tilfellet blir ikke noe mellomnavn gitt, og dermed vil den tomme strengen bli brukt som standardverdi. Det er nyttig å bruke standardverdier for parametere som oftest har en bestemt verdi. Det gir ikke mye mening å bruke standardverdier for parameterne fornavn eller etternavn siden det er så mye variasjon her.

Prøv å ikke bland de to begrepene standardverdier og nøkkelordbaserte argumenter. I koden over så er mellomnavn='' en standardverdi som blir satt i funksjonsdefinisjonen til lag_fullt_navn(). Når vi kaller funksjonen lag_fullt_navn() så bruker vi nøkkelordbaserte argumenter slik som fornavn='Erna'. Dette er to separerte konsepter som har litt lik notasjon, så de er lett å forveksle i begynnelsen. Dette kommer med trening!

6.4 Returverdier

Nå er det passende å se litt nærmere på hvordan vi returnerer verdier fra funksjoner. Noen funksjoner som for eksempel print() har ikke en eksplisitt returverdi. Når funksjoner i Python ikke har en eksplisitt returverdi blir None returnert. Derfor er de to funksjonene under helt like:

def skriv_beskjed():
    print('Dette er en veldig viktig beskjed!')


def skriv_beskjed():
    print('Dette er en veldig viktig beskjed!')
    return None

Hvis du ikke ønsker å returnere noe så trenger du altså ikke skrive return None.

Når bør en funksjon returnere verdier? Dette er helt avhengig av oppgaven funksjonen skal utføre. Under kan du se to funksjoner som begge handler om den midterste verdien i en liste. Den første funksjonen hent_midterste_verdi() returnerer den midterste verdien. Den andre funksjonen overskriv_midterste_verdi() setter den midterste verdien til å være lik null:

def hent_midterste_verdi(liste):
    lengde = len(liste)
    if lengde % 2 == 0:
        print('Listen har ikke en midterste verdi!')
    else:
        return liste[int((lengde + 1) / 2) - 1]


def overskriv_midterste_verdi(liste):
    lengde = len(liste)
    if lengde % 2 == 0:
        print('Listen har ikke en midterste verdi!')
    else:
        liste[int((lengde + 1) / 2) - 1] = 0

I begge funksjonene må vi sjekke om lengden til listen er et partall eller et oddetall. Lister med et partall antall elementer som [1, 3] eller [7, 9, 11, 5] har ikke en midterste verdi. Derimot har lister med et oddetall antall elementer som [3, 1, 3] og [9, 4, 3, 7, 11] en midterste verdi. I funksjonen hent_midterste_verdi() så returnerer vi den midterste verdien. I funksjonen overskriv_midterste_verdi() setter vi den midterste verdien til å være null.

Uttrykket int((lengde + 1) / 2) - 1 er en veldig tungvint måte å regne ut det største heltallet som er mindre enn halvparten til verdien lengde. I mange programmeringsspråk er dette måten vi må regne det på, men i Python kan vi bruke operatoren // som gir oss heltallsdivisjon63. I stedet for å skrive uttrykket int((lengde + 1) / 2) - 1 kan vi i Python bare skrive lengde // 2. Så vi kan forenkle funksjonene over slik:

def hent_midterste_verdi(liste):
    lengde = len(liste)
    if lengde % 2 == 0:
        print('Listen har ikke en midterste verdi!')
    else:
        return liste[lengde // 2]


def overskriv_midterste_verdi(liste):
    lengde = len(liste)
    if lengde % 2 == 0:
        print('Listen har ikke en midterste verdi!')
    else:
        liste[lengde // 2] = 0

Følgende eksempel viser hvordan du kan bruke funksjonen hent_midterste_verdi():

def hent_midterste_verdi(liste):
    lengde = len(liste)
    if lengde % 2 == 0:
        print('Listen har ikke en midterste verdi!')
    else:
        return liste[lengde // 2]


testverdier = [20, 40, 60]
midterste_verdi = hent_midterste_verdi(testverdier)
print(f'Den midterste verdien: {midterste_verdi}')

Funksjonen hent_midterste_verdi() illustrerer også at det er mulig å returnere forskjellige datatyper fra samme funksjon. Dersom listen har partall antall elementer så blir None returnert. Dersom listen har oddetall antall elementer så blir verdien liste[lengde // 2] returnert. Denne verdien kan være enhver type som kan lagres i en liste. Så både heltall, flyttall, strenger, sannhetsverdier, og mye annet kan være returverdier til funksjonen hent_midterste_verdi().

Faktisk kan alle objekter i Python bli returnert av en funksjon. I kapittel 13 skal vi bruke denne generaliteten til å returnere funksjoner fra andre funksjoner. Dette høres jo helt forskrudd ut første gang du hører det. Likevel skal vi se i kapittel 13 at dette er genialt.

Funksjoner som modifiserer data slik som overskriv_midterste_verdi() har ofte ingen eksplisitt returverdi, og returnerer dermed alltid None. Slik kan funksjonen overskriv_midterste_verdi() brukes:

def overskriv_midterste_verdi(liste):
    lengde = len(liste)
    if lengde % 2 == 0:
        print('Listen har ikke en midterste verdi!')
    else:
        liste[lengde // 2] = 0


testverdier = [20, 40, 60]
overskriv_midterste_verdi(testverdier)
print(f'Listen etter modifikasjon: {testverdier}')

Som du kan se så er vi ikke opptatt av å fange opp returverdien til funksjonen overskriv_midterste_verdi(). Denne er bare None uansett så det hadde ikke hjulpet mye. Funksjonen overskriv_midterste_verdi() i koden over endrer listen testverdier permanent etter bruk. Som du ser kan funksjoner uten returverdi være like viktige som dem med returverdi.

En funksjon kan også returnere mer enn en verdi samtidig. Her er et eksempel på en funksjon som henter den første og den siste verdien i en liste:

def første_og_siste_verdi(liste):
    første_verdi = liste[0]
    siste_verdi = liste[-1]
    return første_verdi, siste_verdi


testverdier = [20, 40, 60]
tall1, tall2 = første_og_siste_verdi(testverdier)
print(f'Første verdien: {tall1}')
print(f'Siste verdien: {tall2}')

Funksjonen første_og_siste_verdi() returnerer både det første og det siste elementet i listen den behandler. Vi bruker bare et komma mellom de to verdiene return første_verdi, siste_verdi. Når vi kaller funksjonen skriver vi

tall1, tall2 = første_og_siste_verdi(testverdier)

slik at variabelen tall1 holder verdien som kommer fra første_verdi, mens variabelen tall2 holder verdien som kommer fra siste_verdi.

6.5 Tre triks som hjelper deg med å skrive funksjoner

Når man lærer om funksjoner så finnes det noen nyttige triks det er greit å kjenne til. Disse triksene gjør livet ditt litt enklere. I tillegg gjør triksene at du får en litt bedre forståelse av funksjoner.

6.5.1 Triks 1: Automatisk else-setning

Veldig mange funksjoner har en betinget setning som if-elif-else inni seg der hver gren av if-elif-else-setningen returnerer noe. Her er et typisk eksempel:

def kinopris(vip=False, student=False):
    if vip and student:
        return 125
    elif vip and not student:
        return 150
    elif not vip and student:
        return 100
    else:
        return 120


pris = kinopris(vip=True, student=False)
print(f'Det blir {pris} kroner.')

Funksjonen kinopris regner ut prisen for en kinobillett basert på om kjøperen vil ha VIP-billetter og om kjøperen er en student eller ikke. Det du kan gjøre her for å korte ned koden med en linje er å droppe else-delen slik:

def kinopris(vip=False, student=False):
    if vip and student:
        return 125
    elif vip and not student:
        return 150
    elif not vip and student:
        return 100
    return 120


pris = kinopris(vip=True, student=False)
print(f'Det blir {pris} kroner.')

Kjører du den nye koden med litt forskjellige verdier lagt inn for vip og student så vil du se at dette gir deg akkurat det samme. Hvorfor det?

Dette har å gjøre med at i hver blokk av if-elif-else setningen så blir nøkkelordet return brukt. Sett at betingelsen i enten if-delen eller en av elif-delene blir evaluert til sann. Da blir den grenen utført og nøkkelordet return sørger for at funksjonen avsluttes. Derfor vil bare setningen return 120 bli utført når både if-delen og elif-delene er usanne. Dette er jo akkurat det samme som nøkkelordet else sikrer deg. Derfor kan du droppe nøkkelordet else her og heller bare skrive return 120 direkte etter den siste elif-setningen.

Poenget her er at funksjonen blir avsluttet brått når nøkkelordet return blir brukt. Du kan bruke dette til å slippe å skrive else-delen av if-elif-else-setninger når hver gren har nøkkelordet return i seg.

6.5.2 Triks 2: Utpakking av lister

La oss igjen se litt nærmere på koden som setter sammen et navn bestående av fornavn og etternavn:

def lag_fullt_navn(fornavn, etternavn):
    fullt_navn = f'{fornavn} {etternavn}'
    return fullt_navn


fullt_navn = lag_fullt_navn('Erna', 'Solberg')
print(fullt_navn)

Sett at du fremfor et enkelt navn har en liste som ser slik ut:

navneliste = [
    ['Erna', 'Solberg'],
    ['Jens', 'Stoltenberg']
]

Du ønsker å bruke funksjonen lag_fullt_navn() på hvert par med fornavn og etternavn i listen navneliste. Du kan skrive en for-løkke for å utføre dette slik:

for navn in navneliste:
    fullt_navn = lag_fullt_navn(navn[0], navn[1])
    print(fullt_navn)

Her får bruken av navn[0] og navn[1] koden til å virke mer komplisert enn det den egentlig er. Vi pakker bare ut listen navn inn i funksjonen lag_fullt_navn() for hver iterasjon av for-løkken. Dette kan vi også gjøre ved å skrive *navn slik:

for navn in navneliste:
    fullt_navn = lag_fullt_navn(*navn)
    print(fullt_navn)

Dette kalles utpakking av lister64. Det er en snarvei som gjør at vi slipper å jobbe med indekser når det egentlig ikke er nødvendig. Vi bare pakker ut listen slik at hvert element i listen passer med parameterne i funksjonen.

6.5.3 Tips 3: Nøkkelordet pass

Python har et nøkkelord som heter pass. For å forstå hvordan pass kan hjelpe oss når vi omskriver kode til funksjoner skal vi se på et konkret eksempel. Vi har følgende kode fra kapittel 3 angående priser på bussbilletter som vi ønsker å omskrive til funksjoner:

lengde_reise = float(input('Hvor mange kilometer er reisen? '))
student = bool(int(input('Er passasjeren student? [0 for nei/1 for ja] ')))
alder = int(input('Hvor gammel er passasjeren? '))
pris_reise = 45

if lengde_reise > 30 and not student:
    pris_reise = 60

if alder < 16:
    pris_reise = pris_reise * 0.8
elif alder >= 65:
    pris_reise = pris_reise * 0.75

print(f'Prisen på bussbilletten er {pris_reise} kroner.')

Her er det naturlig å ha en funksjon innhent_informasjon() som henter inn informasjon, og en annen funksjon beregn_pris() som utfører logikken i de betingede setningene. Vi skriver først funksjonen innhent_informasjon(). Etter litt arbeid har vi kommet så langt:

def innhent_informasjon():
    lengde_reise = float(input('Hvor mange kilometer er reisen? '))
    student = bool(int(input('Er passasjeren student? [0 for nei/1 for ja] ')))
    alder = int(input('Hvor gammel er passasjeren? '))
    return lengde_reise, student, alder


def beregn_pris(lengde_reise, student, alder):
    # Skal skrive denne snart


lengde_reise, student, alder = innhent_informasjon()
print(f'Lenge: {lengde_reise}, Studentstatus: {student}, Alder: {alder}')

Som du ser så er innhent_informasjon() ferdig. Funksjonen beregn_pris() har vi bare skrevet skallet til uten funksjonskroppen. Prøv å kjøre koden så langt. Du vil få en IndentationError. Hva er det som er galt?

Funksjoner i Python kan ikke bare være tomme slik som beregn_pris() er foreløpig. Du får en IndentationError fordi Python antar at linjen

lengde_reise, student, alder = innhent_informasjon()

burde vært med i funksjonen beregn_pris() og dermed innrykket til høyre. Hvis vi ønsker en tom funksjon siden vi enda ikke har skrevet funksjonskroppen så kan vi bruke nøkkelordet pass for å fikse dette. Så bytt ut

def beregn_pris(lengde_reise, student, alder):
    # Skal skrive denne snart

med koden

def beregn_pris(lengde_reise, student, alder):
    pass

Kjører du koden nå så skal alt fungere fint. Nøkkelordet pass forteller oss altså at vi skal bare passere forbi funksjonen beregn_pris() fordi den ikke er implementert enda. Til andre som leser koden din så vil nøkkelordet pass også indikere funksjonalitet som ikke ennå er ferdig.

Nøkkelordet pass er veldig vanlig å bruke mens man omskriver kode til funksjoner. Dette gjør det lettere å strukturere koden din tidlig, og du kan teste én funksjon om gangen uten at koden bryter. Nå kan vi teste funksjonen innhent_informasjon() helt til vi er sikker på at den er riktig. Til slutt kan vi omskrive videre slik at også beregn_pris() blir ferdig:

def innhent_informasjon():
    lengde_reise = float(input('Hvor mange kilometer er reisen? '))
    student = bool(int(input('Er passasjeren student? [0 for nei/1 for ja] ')))
    alder = int(input('Hvor gammel er passasjeren? '))
    return lengde_reise, student, alder


def beregn_pris(lengde_reise, student, alder):
    pris_reise = 45
    
    if lengde_reise > 30 and not student:
        pris_reise = 60
    
    if alder < 16:
        return pris_reise * 0.8
    elif alder >= 65:
        return pris_reise * 0.75
    return pris_reise


lengde_reise, student, alder = innhent_informasjon()
pris_reise = beregn_pris(lengde_reise, student, alder)

print(f'Prisen på bussbilletten er {pris_reise} kroner.')

6.6 Tre praksiser for bedre leselighet

Et vanlig ordtak er at du leser mer kode enn du skriver. Når du skriver mer omfattende systemer så vil andre gå gjennom koden du har skrevet for å forstå den. Derfor er det viktig å fokusere på leselighet. Vi har allerede sett dette med PEP 8 for Python kode generelt. I PEP 8 kan du også finne spesifikke retningslinjer for funksjoner som du bør følge. I tillegg til dette er det tre praksiser som jeg kan sterkt anbefale.

6.6.1 Praksis 1: Gode funksjonsnavn

Dårlige funksjonsnavn gjør det vanskelig å forstå hva som foregår. Ofte vil den som leser bare fokusere på der du kaller funksjonen. Selve funksjonsdefinisjonen kan være langt vekke. Tenk deg at du kommer over følgende kode når du leser om et timeføringssystem:

arbeidstimer_ukedager = [7, 9, 6, 7, 4, 0, 0]
ferdig_beregnet = beregn(arbeidstimer_ukedager)

Her forstår du at arbeidstimer_per_dag representerer hvor mange timer en ansatt har jobbet hver dag i en uke. Men hva i all verden gjør funksjonen beregn()? De fleste funksjoner beregner noe, så dette hjelper ikke stort. Variabelnavnet ferdig_beregnet gjør heller ikke livet ditt enklere. Du kan jo gjette, men dette kan fort gå galt. Du må lete frem funksjonsdefinisjonen til beregn() og finner dette:

def beregn(arbeidstimer):
    return sum(arbeidstimer) / 40

Ahh! Så funksjonen beregn() utregner hvor stor andel av en standard 40 timers arbeidsuke en ansatt har jobbet. Nå kan du hoppe tilbake og fortsette å lese videre.

For å slippe at den som leser må gjøre dette så kan vi gi et bedre navn på funksjonen. Her er et eksempel:

def andel_av_arbeidsuke(arbeidstimer):
    return sum(arbeidstimer) / 40
  

arbeidstimer_ukedager = [7, 9, 6, 7, 4, 0, 0]
andel = andel_av_arbeidsuke(arbeidstimer_ukedager)

Bruk både funksjonsnavn og variabelnavn til å formidle hva som foregår. Dette kan også hjelpe deg selv når du returnerer til koden din om en måned. De fleste som har kodet en stund kommer iblant i den pinlige situasjonen der de ikke forstår hva de selv har skrevet. Dette kan skje selv den beste, men vi trenger ikke at det skjer hele tiden.

6.6.2 Praksis 2: Strenger for dokumentasjon

Gode funksjonsnavn hjelper oss med å forstå hva en funksjon gjør. Men ofte er det behov for mer omfattende forklaringer. Det kan være nyttig å forklare:

  • Hva de forskjellige parameterne i funksjonen representerer.

  • Hva funksjonen returnerer.

  • Hvilken oppgave funksjonen har overordnet, og eventuelt hvilken oppgave funksjonen spiller i et større system.

Alt dette er veldig vanskelig å pakke inn i et funksjonsnavn. Heldigvis kan vi i Python skrive en streng på toppen av en funksjonskropp som beskriver funksjonen. Dette kalles en dokumentasjonsstreng65. La oss illustrere dette med funksjonene innhent_informasjon() og beregn_pris() som vi så tidligere i kapittelet. For innhent_informasjon() så kan vi skrive en enkel dokumentasjonsstreng:

def innhent_informasjon():
    """Henter inn informasjon fra den reisende om lengden på reisen, 
    om den reisende er en student, og alderen til den reisende. 
    All den innhentede informasjonen blir returnert."""
    lengde_reise = float(input('Hvor mange kilometer er reisen? '))
    student = bool(int(input('Er passasjeren student? [0 for nei/1 for ja] ')))
    alder = int(input('Hvor gammel er passasjeren? '))
    return lengde_reise, student, alder

Her gir dokumentasjonsstrengen grunnleggende informasjon om formålet til funksjonen. Dette kan være nyttig for leseren slik at det er lettere å forstå hva en funksjon skal oppnå. Iblant er det også nyttig å skrive mer strukturerte dokumentasjonsstrenger der vi beskriver parameterne og returverdien. Dette kan vi gjøre for funksjonen beregn_pris() slik:

def beregn_pris(lengde_reise, student, alder):
    """Beregner prisen til en bussbillett basert på lengden til reisen, 
    studentstatus, og alder til den reisende.
    
    Parameterne:
    lengde_reise (flyttall) -- Lengden på reisen som skal finne sted.
    student (sannhetsverdi) -- Er den reisende er en student eller ikke.
    alder (heltall) -- Alderen til den reisende.
    
    Returnerer prisen til reisen som et flyttall."""
    pris_reise = 45
    
    if lengde_reise > 30 and not student:
        pris_reise = 60
    
    if alder < 16:
        return pris_reise * 0.8
    elif alder >= 65:
        return pris_reise * 0.75
    return pris_reise

Hvis du er oppmerksom ser du at jeg bruker symbolet " og ikke symbolet ' når jeg lager dokumentasjonsstrenger. Det er en konvensjon å bruke " for dokumentasjonsstrenger selv om vi velger å bruke symbolet ' for vanlige strenger. Konvensjoner for dokumentasjonsstrenger finner du ikke i PEP 8, men i PEP 257:

https://peps.python.org/pep-0257/.

Dette er et annet PEP-dokument som utelukkende fokuserer på dokumentasjonsstrenger. Det er en konvensjon å alltid bruke tre doble hermetegn """ på hver side, selv om dokumentasjonsstrengen bare går over én linje. Dette gjør koden mer konsekvent, og det er praktisk fordi det er veldig vanlig å utvide dokumentasjonsstrenger med mer informasjon etter hvert som man bygger ut en funksjon.

Det er ingen konkrete regler for nøyaktig hva som skal være med i en dokumentasjonsstreng. Formålet er at en dokumentasjonsstreng skal gjøre det enklere å forstå funksjonen. Jo lengre og mer kompleks en funksjon er, jo mer dokumentasjon trenger den typisk for å bli forståelig. Likevel er lange og kompliserte funksjoner gjerne et tegn på at du bør bryte dem opp i mindre biter som vi nå skal forklare.

6.6.3 Praksis 3: Funksjoner bør gjøre få oppgaver

Fordelen med funksjoner er at vi kan bokse inne en spesifikk funksjonalitet som kan gjenbrukes. Når en funksjon gjør for mange oppgaver så har dette to ulemper:

  • Det blir vanskeligere å verifisere at funksjonen ikke har noen feil. Flere sammensatte oppgaver kan lettere skjule feil.

  • Det blir vanskeligere å gjenbruke funksjonen. Når en funksjon gjør mange oppgaver så er det mer sjeldent at akkurat den rekkefølgen med oppgaver vil kreves flere ganger.

Her trenger vi å gå gjennom et større eksempel for å forstå dette i praksis. Du jobber ivrig i en butikk som selger bøker, og har et lager for bøker innen sjangeren fantasi. Noen ganger kommer det kunder i butikken som ikke helt vet hva de skal kjøpe. Da kan du foreslå en bok som du har mange eksemplarer igjen av på lageret. På denne måten går du ikke tom unødvendig. Du kan også gi en rabatt basert på hvor mange eksemplarer du har igjen. Hvis du har få eksemplarer igjen så gir du ikke en særlig stor rabatt siden du vil mest sannsynlig få solgt bøkene før et nytt parti ankommer. Her er kode som kan hjelpe deg med å utregne dette automatisk:

fantasibøker_lager = [
    ['Ringenes Herre', 7, 149.90],
    ['Dune', 3, 199.90],
    ['Eragon', 20, 99.90],
    ['Kampen om Jerntronen', 12, 149.90]
]

# Sjekk at lageret ikke er helt tomt
eksemplarer_igjen = False
for bok in fantasibøker_lager:
    if bok[1] != 0:
        eksemplarer_igjen = True
        break

# Hent ut boken som det er flest eksemplarer av
flest_eksemplarer_tittel = ''
flest_eksemplarer_antall = 0
flest_eksemplarer_pris = 0
if eksemplarer_igjen:
    for bok in fantasibøker_lager:
        if bok[1] > flest_eksemplarer_antall:
            flest_eksemplarer_tittel = bok[0]
            flest_eksemplarer_antall = bok[1]
            flest_eksemplarer_pris = bok[2]

# Tilby en rabatt basert på hvor mange eksemplarer det er igjen i lageret
if eksemplarer_igjen:
    rabatt_faktor = 1 - flest_eksemplarer_antall / 100
    rabattpris = round(flest_eksemplarer_pris * rabatt_faktor, 2)
    print(f'Rabattpris på {flest_eksemplarer_tittel} er {rabattpris} kroner.')

Les koden over nøye. Funksjonen round() har du sett i kapittel 2 med ett enkelt argument. Her setter vi først inn argumentet vi ønsker å avrunde, og deretter et argument som beskriver hvor mange desimaler vi ønsker. Dette gir oss mer kontroll over avrunding og er ofte nyttig.

Koden fungerer fint, men du ønsker å omskrive den til funksjoner. En måte å gjøre dette på er å skrive en enkelt funksjon som gjør all funksjonaliteten:

fantasibøker_lager = [
    ['Ringenes Herre', 7, 149.90],
    ['Dune', 3, 199.90],
    ['Eragon', 20, 99.90],
    ['Kampen om Jerntronen', 12, 149.90]
]


def utregn_rabattpris(lager):
    """Regner ut rabattpris på den boken vi har flest eksemplarer av."""
    
    # Sjekk at lageret ikke er helt tomt
    eksemplarer_igjen = False
    for bok in lager:
        if bok[1] != 0:
            eksemplarer_igjen = True
            break

    # Hent ut boken som det er flest eksemplarer av
    flest_eksemplarer_tittel = ''
    flest_eksemplarer_antall = 0
    flest_eksemplarer_pris = 0
    if eksemplarer_igjen:
        for bok in lager:
            if bok[1] > flest_eksemplarer_antall:
                flest_eksemplarer_tittel = bok[0]
                flest_eksemplarer_antall = bok[1]
                flest_eksemplarer_pris = bok[2]

    # Tilby en rabatt basert på hvor mange eksemplarer det er igjen i lageret
    if eksemplarer_igjen:
        rabatt_faktor = 1 - flest_eksemplarer_antall / 100
        rabattpris = round(flest_eksemplarer_pris * rabatt_faktor, 2)
        return flest_eksemplarer_tittel, rabattpris


valgt_bok, rabattpris = utregn_rabattpris(fantasibøker_lager)
if rabattpris:
    print(f'Prisen etter rabatt på {valgt_bok} er {rabattpris} kroner.')

Les igjen koden over nøye. Vi har nå laget en funksjon utregn_rabattpris() som utfører tre oppgaver:

  • Funksjonen utregn_rabattpris() sjekker at lageret ikke er helt tomt.

  • Funksjonen utregn_rabattpris() henter ut boken med flest eksemplarer.

  • Funksjonen utregn_rabattpris() tilbyr en rabatt på boken med flest eksemplarer.

Funksjonen utregn_rabattpris() gjør altfor mye. Problemet er at for å gjenbruke funksjonen utregn_rabattpris() i andre deler av systemet vårt så må alle 3 oppgavene over være krevd. Det kan godt hende at vi andre steder kun ønsker å sjekke om lageret er tomt. Et eksempel er for å automatisk sende en hastemelding til leverandør om at vi trenger nye bøker raskt.

Gjenbruker vi funksjonen utregn_rabattpris() vil de to andre oppgavene bli utført i tillegg. Dette er for det første bortkastet arbeid. I tillegg blir koden ekstremt kryptisk andre steder den blir brukt. Slik gjenbruk er starten på en kodebase som fort kan bli helt uforståelig.

Så hva er løsningen? Funksjoner bør så langt det er mulig gjøre få oppgaver. I vårt eksempel kan vi lage tre forskjellige funksjoner som gjør de forskjellige oppgavene slik:

fantasibøker_lager = [
    ['Ringenes Herre', 7, 149.90],
    ['Dune', 3, 199.90],
    ['Eragon', 20, 99.90],
    ['Kampen om Jerntronen', 12, 149.90]
]


def sjekk_lager(lager):
    """Sjekker om lageret er tomt. 
    Returnerer True om det er noe igjen på lageret."""
    for bok in lager:
        if bok[1] != 0:
            return True
    return False


def flest_eksemplarer(lager):
    """Returnerer tittel, antall eksemplarer, og pris 
    til boken med flest eksemplarer."""
    tittel, antall, pris = '', 0, 0
    for bok in lager:
        if bok[1] > antall:
            tittel, antall, pris = bok[0], bok[1], bok[2]
    return tittel, antall, pris


def utregn_rabattpris(eksemplarer, pris):
    """Regner ut en passende rabatt basert på antall eksemplarer på lager."""
    return round(pris * (1 - eksemplarer / 100), 2)


if sjekk_lager(fantasibøker_lager):
    print(f'Det er eksemplarer igjen i lageret.')
    tittel, eksemplarer, pris = flest_eksemplarer(fantasibøker_lager)
    print(f'Boken {tittel} har vi {eksemplarer} eksemplarer av.')
    rabattpris = utregn_rabattpris(eksemplarer, pris)
    print(f'Prisen etter rabatt på {tittel} er {rabattpris} kroner.')

Hver av de tre funksjonene får en egen dokumentasjonsstreng som forklarer funksjonaliteten. Det er nå mulig å bruke funksjonaliteten i sjekk_lager() til å sjekke om lageret er tomt uavhengig av de to andre oppgavene. Dette gir oss mulighet til gjenbruk, som er hovedpoenget med funksjoner.

En annen fordel er at variabelnavn ofte blir enklere fordi vi har en mer begrenset kontekst. Tidligere når vi bare hadde en funksjon utregn_rabattpris() så ble vi nødt til å bruke variabelnavn som flest_eksemplarer_tittel, flest_eksemplarer_antall, og flest_eksemplarer_pris. Nå bruker vi disse variablene inni funksjonen flest_eksemplarer(). Her har vi kontekst om at situasjonen handler om bøker med flest eksemplarer. Da holder det å bruke variabelnavnene tittel, antall, og pris. Dette gjør variabelnavnene så korte at det er leselig å sette alle tre variablene på en enkelt linje.

Poenget er at funksjoner skal ikke gjøre for mye. Pass på at du ikke utvider funksjonene dine gradvis slik at de tar på seg flere og flere oppgaver. En funksjon som gjør en enkel oppgave er lett å forstå, lett å gjenbruke, og lett å stole på.

6.7 Prosjekt - Handlelisteassistent del 2

Vi skal nå bruke det vi har lært om både løkker og funksjoner til å ferdigstille applikasjonen vi jobbet med i seksjon 3.7. Koden vi endte opp med så slik ut:

handleliste = ['Eple', 'Pære', 'Chips']
handling = input('Legge til, fjerne, eller skrive ut varer? (L/F/S): ')

if handling.upper() == 'L':
    vare = input('Legg til en ny vare til handlelisten: ')
    handleliste.append(vare)
elif handling.upper() == 'F':
    vare = input('Vare i handlelisten som skal fjernes: ')
    handleliste.remove(vare)
elif handling.upper() == 'S':
    sortert_handleliste = sorted(handleliste)
    print(f'Handlelisten er: {sortert_handleliste}')
else:
    print('Dette er ikke et gyldig input. Prøv igjen!')

Utfordringen vi hadde var at programmet vårt ikke hadde noen hukommelse. Hver gang vi kjører programmet tilbakestilles listen handleliste til sin originale verdi. Dette er ikke en veldig nyttig applikasjon for en handleliste. For å fikse dette må vi bruke en while-løkke som vi lærte om i kapittel 5. Vi trenger en while-løkke og ikke en for-løkke siden vi ikke vet hvor mange handlinger brukeren ønsker å gjøre på forhånd. Etter dette vil det være behov for å omskrive programmet i form av funksjoner.

6.7.1 Legge til en while-løkke

Vi kan starte med en uendelig while-løkke slik:

handleliste = []

while True:
    handling = input('Legge til, fjerne, eller skrive ut varer? (L/F/S): ')

    if handling.upper() == 'L':
        vare = input('Legg til en ny vare til handlelisten: ')
        handleliste.append(vare)
    elif handling.upper() == 'F':
        vare = input('Vare i handlelisten som skal fjernes: ')
        handleliste.remove(vare)
    elif handling.upper() == 'S':
        sortert_handleliste = sorted(handleliste)
        print(f'Handlelisten er: {sortert_handleliste}')
    else:
        print('Dette er ikke et gyldig input. Prøv igjen!')

Hvis du kjører koden over, vil du kunne legge til, fjerne, og skrive ut listen flere ganger. Dette representerer et stort steg fremover. Likevel har vi problemer med at programmet aldri slutter. En god kommandolinje-applikasjon kan stoppes på mer skånsomme måter enn å trykke Control + C på Windows eller Cmd + C på Mac. For å gjøre dette så legger vi inn en mulighet for å bryte ut av løkken slik:

handleliste = []

print('Dette er en applikasjon som lar deg jobbe med en handleliste!')
print('Du kan alltid skrive \'exit\' for å stenge applikasjonen')

while True:
    handling = input('Legge til, fjerne, eller skrive ut varer? (L/F/S): ')

    if handling.upper() == 'L':
        vare = input('Legg til en ny vare til handlelisten: ')
        handleliste.append(vare)
    elif handling.upper() == 'F':
        vare = input('Vare i handlelisten som skal fjernes: ')
        handleliste.remove(vare)
    elif handling.upper() == 'S':
        sortert_handleliste = sorted(handleliste)
        print(f'Handlelisten er: {sortert_handleliste}')
    elif handling.upper() == 'EXIT':
        print('Takk for at du brukte applikasjonen vår. På gjensyn!')
        break
    else:
        print('Dette er ikke et gyldig input. Prøv igjen!')

Her har vi brukt funksjonen print() to ganger på toppen av programmet. Her får brukeren informasjon om at det er mulig å skrive inn 'exit' for å stenge programmet. Dersom brukeren skriver inn 'exit' med små eller store bokstaver så bruker vi nøkkelordet break til å bryte ut av løkken. Så enkelt er det. Dermed er grunnfunksjonaliteten i programmet på plass.

6.7.2 Omskriving av programmet til funksjoner

Selv om programmet er ferdig per nå så skal det muligens utvides i fremtiden. Kanskje du ønsker å skrive handlelisten til en fil? Dette skal vi lære om i kapittel 10. Kanskje du ønsker å bedre håndtere feilene som kan dukke opp i programmet? Dette skal vi lære om i kapittel 12. Uansett hva grunnen er, er det vanlig å utvide programmer.

Jo lengre programmet vårt blir, jo mer komplisert blir det å utvide det hvis vi ikke omskriver koden til funksjoner. La oss få litt ekstra trening med funksjoner slik at vi kan gjøre applikasjonen vår mer robust. Det er jo i hovedsak de tre handlingene knyttet til å legge til varer, fjerne varer, eller skrive ut handlelisten som er nyttig å skrive om til funksjoner. Grunnen er at det er her vi forventer utvidelser kan finne sted. I tillegg så er det vanlig å skrive en funksjon som heter main() som går gjennom hovedfunksjonaliteten til programmet.

def legge_til_vare(handleliste):
    vare = input('Legg til en ny vare til handlelisten: ')
    handleliste.append(vare)


def fjerne_vare(handleliste):
    vare = input('Vare i handlelisten som skal fjernes: ')
    handleliste.remove(vare)


def skrive_ut_handleliste(handleliste):
    sortert_handleliste = sorted(handleliste)
    print(f'Handlelisten er: {sortert_handleliste}')


def main():
    handleliste = []
    print('Dette er en applikasjon som lar deg jobbe med en handleliste!')
    print('Du kan alltid skrive \'exit\' for å stenge applikasjonen')

    while True:
        handling = input('Legge til, fjerne, eller skrive ut varer? (L/F/S): ')

        if handling.upper() == 'L': legge_til_vare(handleliste)
        elif handling.upper() == 'F': fjerne_vare(handleliste)
        elif handling.upper() == 'S': skrive_ut_handleliste(handleliste)
        elif handling.upper() == 'EXIT':
            print('Takk for at du brukte applikasjonen vår. På gjensyn!')
            break
        else:
            print('Dette er ikke et gyldig input. Prøv igjen!')


main()

Merk at vi bruker funksjonen main() på slutten av programmet. Det er funksjonen main() som starter programmet. Den kjører gjennom hovedlogikken til programmet vårt. Inni funksjonen main() blir de andre funksjonene legge_til_vare(), fjerne_vare(), og skrive_ut_handleliste() brukt når det er nødvendig.

6.8 Oppgaver

Oppgave 1

Hva gjør følgende kode? Hva vil hver av de tre forsøkene skrive ut i terminalen?

def utregne_gjennomsnitt(liste):
    """Regner ut gjennomsnittet av en liste med tall."""
    gjennomsnitt = sum(liste) / len(liste)
    print(f'Gjennomsnittet er: {gjennomsnitt}')


# Forsøk 1
utregne_gjennomsnitt([4, 8, 15, 16, 23, 42])

# Forsøk 2
print(utregne_gjennomsnitt([4, 8, 15, 16, 23, 42]))

# Forsøk 3
utregne_gjennomsnitt([])

Oppgave 2

Skriv en funksjon bestill_mat() som tar inn følgende parametere:

  • Parameteren retter obligatorisk.

  • Parameteren drikke er valgfri og har standardverdien 'vann'.

  • Parameteren dessert er valgfri og har standardverdien None.

  • Parameteren takeaway er valgfri og har standardverdien False.

Funksjonen bestill_mat() skal returnere en streng som beskriver bestillingen. Her ser du noen eksempler på hvordan den skal se ut:

print(bestill_mat('pizza'))
# Du har bestilt pizza med vann til å drikke.

print(bestill_mat('sushi', drikke='grønn te', dessert='iskrem'))
# Du har bestilt sushi med grønn te til å drikke og iskrem til dessert.

print(bestill_mat('burger', takeaway=True))
# Du har bestilt burger med vann til å drikke. Takeaway!

print(bestill_mat('taco', drikke='cola', dessert='brownie', takeaway=True))
# Du har bestilt taco med cola til å drikke og brownie til dessert. Takeaway!

Oppgave 3

Du skal lage en funksjon kaffetips() som gir råd til programmerere om de skal ta seg en kaffekopp til eller ikke. For lite kaffe kan for dem som drikker det bety lavere produktivitet. For mye kaffe kan derimot gjøre deg rastløs. Du baserer funksjonen på de følgende to parameterne:

  • Parameteren antall_kopper er et heltall som beskriver hvor mange kaffekopper personen allerede har drukket.

  • Parameteren fokusert er en sannhetsverdi som beskriver om personen føler seg fokusert eller ikke.

Her ser du en tabell som beskriver hvilken råd som bør gis basert på verdiene som blir sendt til antall_kopper og fokusert:

Antall Kopper Fokusert Råd
Mindre eller lik 2 False Ta en solid kopp kaffe!
Mindre eller lik 2 True Ta deg en kopp kaffe om en halvtime!
Større enn 2 False Ta deg en siste kopp kaffe, men ikke mer!
Større enn 2 True Du trenger ikke mer kaffe!

Skriv funksjonen kaffetips() ved å bruke en automatisk else-setning, slik som beskrevet i det første tipset i seksjon 6.5. Husk å teste funksjonen etterpå for å forsikre deg om at den gjør det du forventer.

Oppgave 4

Du har fått måledata fra seismiske apparater, representert som en liste med flyttall. En annen utvikler har skrevet en funksjon som behandler måledataene slik:

def behandle_data(måledata):
    ny_måledata = []
    for datapunkt in måledata:
        if datapunkt < 1:
            ny_måledata.append(0)
        else:
            ny_måledata.append(datapunkt)
    
    ny_ny_måledata = []
    for datapunkt in ny_måledata:
        ny_ny_måledata.append(round(datapunkt))
    return ny_ny_måledata

Som du ser, er det betydelig rom for å forbedre lesbarheten i koden. Din oppgave er:

  • Dele funksjonen behandle_data() opp i to mindre funksjoner som hver løser én tydelig deloppgave.

  • Gi de to nye funksjonene beskrivende og presise navn.

  • Bruk meningsfulle variabelnavn i hver av funksjonene.

  • Legg til dokumentasjonsstrenger som forklarer hva hver funksjon gjør.

Oppgave 5 (Utfordrende)

Du kan bruke den innebygde funksjonen max() med så mange argumenter du ønsker. Bare se her:

print(max(2, 3))
# 3

print(max(2, 3, 1, 7, 2))
# 7

print(max(2, 3, 1, 7, 2, -1, 13, 12, 2))
# 13

Hvordan kan vi skrive en funksjon som kan ta et vilkårlig antall argumenter? Vi kan skrive *args som en parameter. Dette er en indikasjon på at når vi kaller funksjonen så kan vi spesifisere så mange posisjonsbaserte argumenter vi ønsker:

def beskriv_person(navn, *args):
    print(f'{navn} har følgende hobbyer:')
    for hobby in args:
        print(f'- {hobby}')


beskriv_person('Anna', 'Løping', 'Piano', 'Strikking')

Du trenger ikke kalle parameteren for *args så lenge den starter med symbolet *. Så i eksempelet over kunne du skrevet *hobbyer for å gjøre det enklere å forstå:

def beskriv_person(navn, *hobbyer):
    print(f'{navn} har følgende hobbyer:')
    for hobby in hobbyer:
        print(f'- {hobby}')


beskriv_person('Anna', 'Løping', 'Piano', 'Strikking')

Oppgaven din er å skrive en funksjon som heter lengste_ord(). Funksjonen lengste_ord() skal ta inn en vilkårlig mengde ord og returnere ordet som er lengst:

print(lengste_ord("banan", "eple", "kiwi", "vannmelon", "plomme", "melon"))
# vannmelon