Göm menyn

961G24 Programmering i text-baserad miljö

Funktioner

Inledning

Nu har vi kommit en bit. En bra bit faktiskt. Med variabler, tilldelning, uttryck, if-satser och loopar kan vi faktiskt lösa alla lösbara problem. Det finns dock två små problem som vi fortfarande måste tampas med. Dessa två problem är helt praktiska till sin natur och är egentligen två fasetter av samma problem. Problemet har med komplexitet att göra.

Moderna program är stora. Hittils har våra program varit i storleksordningen 10-100 rader. Men ett normalt datorprogram är ju flera magnituder större! Ta t.ex. ett vanligt datorprogram som Firefox. Det har ca 50 miljoner rader kod. Inte så konstigt kanske, det är ju ett avancerat program. Men hur, i hela fridens namn, skall man som programmerare kunna producera eller ens förstå, 50 miljoner rader kod? Med bara de verktyg vi har lärt oss hittils går inte detta. Strax efter 100 rader börjar vi bli snurriga och tycka att det är jobbigt.

Hur löser vi då detta? Jo, vi inser ganska snart att många av de saker som vi gör i våra program återkommer ofta. Det kanske är en snutt kod på 5-10 rader som vi har med i nästan alla våra program, flera gånger kanske. I detta fall vore det ju fin fint att kunna packetera dessa rader kod, sätta ett namn på dem, lägga dem "vid sidan av" resten av programmet. När vi vill köra de raderna så säger vi bara det namn som vi hittade på. Då har vi ersatt dessa rader med endast ett kommando! Detta skulle hjälpa oss på två sätt. För det första så skulle koden krympa avsevärt, vilket är önskvärt. För det andra så är det troligtvis lättare att förstå vårt samlingsnamn i den kontext där det står istället för de ursprungliga raderna kod. Vi har gjort programmet mer abstrakt, men enklare att förstå. Vi kallar detta för att skapa abstraktion i koden.

Just detta är tanken bakom underprogram. Och det är det som gör det praktiskt möjligt att komma vidare inom programmering. Här kommer ett kort exempel:


def stars():
  print('**************************')

print("Treans multiplikationstabell:")
stars()
for i in range(10):
  print(i*3, end=' ')
print()
stars()

Vid utskriften av treans multiplikationstabell vill vi att programmet skriver ut ett antal stjärnor ovanför och under tabellen. Nu har vi delat upp programmet i två delar: ett huvudprogram som börjar vid utsrkiften "Treans multiplikationstabell" (som vanligt är det här python börjar köra när vi kör programmet), och ett underprogram (som vi har döpt till starts) som bara körs då vi anropar det (genom att skriva namnet på det). Efter att stjärnorna skrivits ut hoppar programmet tillbaka dit det var och fortsätter köras.

Funktioner

Underprogram i python heter funktioner. Detta låter som det matematiska begreppet, och det finns självklart likheter, men också skillnader som vi kommer att se.

Vi utgår ifrån följande lilla program:


a = int(input('Mata in ett tal: '))
b = int(input('Mata in ett tal: '))
c = int(input('Mata in ett tal: '))

storsta = a
if b > storsta:
    storsta = b

if c > storsta:
    storsta = c

print('Största tal var: ', end='')
print(storsta)

Även om detta är ett litet program, lätt att läsa och förstå, så ser vi tendensen som nämdes ovan. "Samma" kod kommer flera gånger. Både vid inmatningen och vid if-satserna. Vi fokuserar på det senare. Vi skulle kunna skriva om det med en funktion på detta sätt:


# Underprogram
def max(x, y):
    if x > y:
        return x
    else:
        return y

# Huvudprogram
a = int(input('Mata in ett tal: '))
b = int(input('Mata in ett tal: '))
c = int(input('Mata in ett tal: '))

storsta = max(a, b)           # anrop till funktionen
storsta = max(storsta, c)     # ett till anrop

print('Största tal var: ', end='')
print(storsta)

När vi anropar max första gången så kopieras värdet i variabeln a till x och värdet i b till y. Vi kallar x och y för parametrar. Sedan börjar underprogrammets kod köras, samtidigt som huvudprogrammet väntar. Inne i underprogrammet gör vi det som tidigare låg i huvudprogrammet, nämligen jämför vilket av talen som är störst. Skulle värdet i x vara större än värdet i y så går vi in och returnerar värdet av x. När något returneras från en funktion så hoppar man direkt tillbaka till huvudprogrammet. Man kan då tänka sig att anropet (d.v.s. "max(a, b)") ersätts med det returnerade värdet. Alltså kommer variablen storsta sättas till det som x var. Om y hade varit större än x (eller lika stort) så hade värdet i y returneras.

Nu är det viktigt att poängtera några saker:

  • Vi använder ordet "def" för att definiera en ny funktion.
  • Funktionen läggs längst upp i filen, ovanför huvudprogrammet (men under ev. import-satser som vi vill skall gälla för hela filen.)
  • Koden inne i funktionen indenteras, precis som vi gör för if-satser och loopar.
  • Parametrarna x och y finns bara inne i funktionen max, inte i huvudprogrammet. Detta är bra, för det betyder att vi inte bara kan lägga kod "vid sidan av" utan även variabler som den koden behöver för att fungera.
  • När man anropar en funktion så överförs det som står i parentesen till parametrarna, i ordning. Namnen på parametrarna behöver inte vara samma som namnen på variablerna i huvudprogrammet, men de får ha samma namn!
  • Vid andra anropet till vår funktion så överförs värdet i variabeln storsta till x, och värdet i variabeln c till y. Nu ser vi att det verkligen är fiffigt, för inne i max så behöver vi inte bry oss om omvärlden. Vi vet att vi får in två värden och skall leverera ut det största av de två. Vi har alltså inte bara förenklat huvudprogrammet utan även förenklat lösandet av själva max-problemet, eftersom man däri inte behöver ta hänsyn till något annat.
  • Funktionen max behöver vi faktiskt inte skriva, eftersom den finns i modulen math. Men det visar i alla fall på hur användbara funktioner är. När man tänker på det så har vi ju redan använt massor av funktioner: print, input, int, range o.s.v. Tänk hur komplicerade våra program skulle bli om vi behövde klara oss utan dem!

Som tidigare nämnt så finns det många likheter mellan vår funktion och dess namne i matematiken. Skillnaden här är snarare att vi måste tänka oss att detta är något som mekaniskt körs, inte är ett förhållande eller en ekvation som gäller för alltid.

Fler varianter

Som vi såg i första exemplet så måste en funktion inte returnera något och kan ha ett godtyckligt antal parametrar, även noll. Säg att vi har ett program som skall presentera en meny och låta användaren välja. Själva utskriften av menyn kommer bli ett par print:ar - vi kan lägga dem i en funktion:


def print_menu():
    print('1. Oxbringa med pepparrotssås.')
    print('2. Gratinerad lax med rotmos.')
    print('3. Stuvade rotsaker och saffransmousse')
    print('4. Kantarellsoppa och vitlökstoast.')

# Huvudprogram
print('Välkommen till Bistro de Magnifique! Vad får det lov att vara?')
print_menu()
choice = int(input('Gör ditt val: '))

Det vi ser här är att print_menu aldrig gör return. Och det finns ingen variabel som "tilldelas anropet" nere i huvudprogrammet. Det finns inte heller några parametrar eller variabler som skickas in till print_menu, men vi har fortfarande ett tomt parentespar för att påminna oss om att detta är ett anrop (i huvudprogrammet). Efter sista print-satsen i underprogrammet så återvänder man till huvudprogrammet där anropet är och fortsätter därifrån.

Vi kan även ha funktioner som returnerar flera saker. Säg t.ex. att vi vill att våra variabler skall byta innehåll med varandra:


def swap(a, b):
    return b, a

# Huvudprogram
x = int(input('Mata in ett x: '))
y = int(input('Mata in ett y: '))
x, y = swap(x, y)
print('x är nu {0}'.format(x))
print('y är nu {0}'.format(y))

Vi antar att användaren matar in 3 och 7. Vid anropet så kommer en 3:a kopieras in i a och en 7:a kommer in i b. Vi returnerar sedan dessa tal, men i omvänd ordning. I huvudprogrammet tar vi emot dem med x först sedan y. Alltså får x värdet 7 och y får värdet 3.

Här brukar vissa säga: "Men varför inte flytta in print:arna till funktionen också? Då blir ju huvudprogrammet ännu kortare!". Detta vore dock inte bra, eftersom vi då "förstör" vår funktion. Om funktionen skriver ut de ombytta värdena istället för att returnera dem så kan vi ju inte använda vår swap för tillämpningar där vi inte vill skriva ut variablerna. Kort sagt: man vill att sina funktioner skall lösa en så liten uppgift som möjligt. Ta in data på ett generellt sätt (via parametrar, inte med input), lösa problemet och skicka ut resultatet på ett gernerellt sätt (med return, inte med print). På så sätt får vi en funktion som är så generell och allmän som möjligt, och som kan användas till många olika saker.

Övningar

Uppgift 1:

Skriv ett program som låter användaren mata in ett heltal N. Skriv sedan en funktion. Som tar N som parameter och beräknar summan av 1...N. Resultatet skall returneras till huvudprogrammet där det skrivs ut.

Uppgift 2:

Skriv ett program som låter användaren mata in en temperatur i grader Celcius. Skriv sedan en funktion to_farenheit som tar temperaturen som parameter, beräknar vad motsvarande temperatur är i grader Farenheit och returnerar det till huvudprogrammet, där det skrivs ut.

Uppgift 3:

Skriv en funktion som låter användaren mata in ett heltal. Om heltalet är negativt så skall användaren mata in igen, tills användaren matar in ett icke-negativt värde. Det icke-negativa värdet skall returneras från funktionen. Testa din funktion med ett lämpligt huvudprogram.

Körexempel:

Mata in ett tal (>= 0): -3
Mata in ett tal (>= 0): -10
Mata in ett tal (>= 0): 13

Du matade in 13

Flera funktioner

Vill man ha flera funktioner i sitt program kan man lägga dem efter varandra i koden (ovanför huvudprogrammet). Tänk även på att funktioner kan anropa varandra. Man kan t.ex. låta huvudprogrammet anropa ett underprogram, som i sin tur anropar ett eller flera andra underprogram för att lösa sitt delproblem.

Att tänka på

Precis som att huvudprogrammet kan anropa en funktion så kan även en funktion anropa en annan funktion. Detta betyder att vi nu kan bryta upp vårt program i små delar som är enkla att förstå och skapa. Vi kan strukturera vårt program i en hierarki. Det svåra ligger endast i att veta vad som skall brytas ut till en funktion och vad som kan stanna kvar i huvudprogrammet. Här finns det olika sätt att tänka, och det är egentligen mest en erfarenhetsgrej, men vi kan sätta upp ett par tumregler:

  • En funktion bör bara lösa en uppgift, inte flera.
  • En funktion bör inte (utan god anledning) vara längre än tjugo rader kod. När den börjar bli större än så är det dags att bryta upp även denna i mindre funktioner.
  • En funktion bör ha ett namn som tydligt beskriver vad den gör, men inte hur den gör det.

Ett annat sätt att tänka på är följande (som vi kan kalla för söndra-och-härska-metoden).

Givet att du har ett problem P som skall lösas.

  • Om P är ett trivialt:
      lös direkt (behövs inga underprogram)
    
  • Om P inte är trivialt:
      Bryt upp P i lämpliga mindre bitar P1, P2 ... PN.
      Applicera söndra-och-härska på vart och ett av P1 ... PN
    

Sidansvarig: Pontus Haglund
Senast uppdaterad: 2020-09-10