Lektion 4: Att skriva funktioner#

Syfte: Öva på funktioner och algoritmer

Innehåll: Funktionsdefinitioner, parametrar, returvärden, multipla returvärden

Arbetsform: Arbeta gärna tillsammans med någon men skriv egen kod. Diskutera med varandra! Bäst är att försöka lösa uppgifterna innan du tittar på svaren! Läs dock alltid svaren noga - en del innehåller information som inte står någon annanstans! Fråga handledarna om det är något du inte förstår!

Uppskattad arbetstid: 4 timmar.

Redovisning: Ingen obligatorisk redovisning men diskutera med handledare om det är något du är osäker på!


Funktioner#

I matematiken definieras en funktion som en avbildning från en mängd, ”definitionsmängden”, till en annan mängd, ”värdemängden”. Exempel är bl.a. de trigonometriska funktionerna som sin, cos, … Avbildningen ska vara entydig, dvs. samma värde från definitionsmängden ska alltid ge samma värde ur värdemängden.

I programmeringsvärlden använder man sig (i de flesta språken) av en vidare tolkning av begreppet funktion: de behöver inte ha någon definitionsmängd, de behöver inte ha någon värdemängd och de behöver inte heller vara entydiga.

Av de inbyggda funktioner du (troligen) sett är

  • abs ett exempel på en funktion som har både värdemängd och definitionsmängd och dessutom är entydig,

  • print ett exempel på en funktion utan värdemängd, och

  • input ett exempel på en funktion som inte är entydig - den kan ju ge olika resultat vid olika anrop med samma argument.

En funktion i programmeringssammanhang är en samling instruktioner som utför någon viss uppgift. Funktioner har ett namn som man använder när man vill att dessa instruktioner ska utföras. Man kan skicka information till funktionen via parametrar och funktioner kan, men behöver inte, returnera resultat som funktionsvärde. Funktioner som inte returnerar ett resultat har i regel någon sidoeffekt som t.ex. att skriva något i ett terminalfönster eller flytta på en padda.

Vi har ovan sagt att funktioner inte nödvändigtvis behöver returnera något värde. I Python är det dock så att funktioner alltid returnerar ett värde. Om man inte explicit returnerar ett värde så kommer funktionen returnera värdet None.

Se Wikipedia för en diskussion av funktionsbegreppet i programmeringsvärlden.

Python har ett stort antal fördefinierade funktioner. Några finns allmänt tillgängliga (t.ex. abs, print, input, …) men de flesta finns i paket som t.ex. math.

Detta kapitel handlar om hur man skriver och använder egna funktioner.

Definiera egna funktioner#

Ordet def inleder funktionsdefinitionen. Därefter följer funktionens namn och eventuella parametrar inom parentes. Parametrarna åtskiljs av kommatecken. Parentesparet måste finnas med även om funktionen inte har några parametrar. Efter den inledande raden följer funktionskroppen, dvs. de satser som ska utföras när funktionen anropas. Funktionskroppen skrivs indenterat med 4 blanksteg. Funktionsdefinitionen är slut vid första oindenterade sats.

Allmänt utseende:

def namn(p1, p2, p3...):
    s1
    s2
    s3

där p1, p2, … är parameternamn och s1, s2, … är godtyckliga Python-satser.

I funktionskroppen kan en eller flera return-satser förekomma. När en sådan utförs återgår exekveringen till det ställe som anropade funktionen. Om return-satsen innehåller ett uttryck beräknas detta och värdet skickas tillbaka som funktionsvärde.

Funktioner med parametrar och returvärde#

Exempel: Konvertering från Fahrenheit till Celsius#

Funktionsdefinition

Kommentarer

def fahrenheit_to_celsius(f):
    return (5/9) * (f - 32)
  • Funktionen heter fahrenheit_to_celsius. Enligt stilguiden ska namnet skrivas med gemena (små) bokstäver. Understrykningstecknet kan användas för att öka läsbarheten.

  • Funktionen har en parameter f som anges inom parenteser.

  • Sist kommer en return-sats. Satsen innehåller ett uttryck som beräknas och vars värde skickas tillbaka som funktionsvärde.

Exempel: Konvertering från Fahrenheit till Kelvin#

Funktionsdefinition

Kommentarer

def fahrenheit_to_kelvin(f):
    c = fahrenheit_to_celsius(f)
    return c + 273.16
  • Funktionen har en parameter f.

  • Funktionen använder den ovan skrivna funktionen för att konvertera till grader Celsius.

  • Funktionen har en lokal variabel c. Den tilldelas sitt värde genom en beräkning.

  • Även här innehåller return-satsen ett uttryck som beräknas och vars värde returneras som funktionsvärde.

  • Det är inte alls nödvändigt att använda en lokal variabel — vi hade lika gärna kunnat skriva hela uttrycket fahrenheit_to_celsius(f) + 273.16 i return-satsen.

  • Variablerna f och c existerar inte utanför funktionskroppen.

Exempel på användning av konverteringsfunktionerna

Utskrift

Kommentar

print(fahrenheit_to_celsius(50))

10.0

Resultatet blir av flyttalstyp eftersom divisionsoperatorn / alltid ger ett flyttal

print(fahrenheit_to_kelvin(32))

273.16

x = 2
print(fahrenheit_to_celsius(x + 3))

-15.0

Argumentet, dvs. uttrycket x + 3, beräknas först till 5 som är det värde som skickas till parametern.

Övningar#

  1. Skriv en funktion som tar en temperatur i grader Celsius och returnerar temperaturen i grader Fahrenheit.

    Svar
    def celsius_to_fahrenheit(c):
        return 9/5*c + 32
    

    Det går alltså att skriva hela uttrycket i return-satsen. Ingen lokal variabel behövs då.

  2. Klistra in

    def fahrenheit_to_celsius(f):
        c = (5/9) * (f - 32)
        return c
    
    
    print(fahrenheit_to_celsius(77))
    

    Kör programmet och undersök vad variablerna f och c har för värde efter print-satsen!

    Vad kan man dra för slutsats av detta?

    Svar

    Variablerna f och c existerar inte utanför funktionskroppen!

    Detta är ett viktigt faktum! Parametrarna och lokala variabler finns bara lokalt. Man kan alltså utan risk för sammanblandning använda samma namn utanför funktionen och i andra funktioner.

Exempel: Funktion med flera satser och lokala variabler#

En funktion för att beräkna den harmoniska summan 1 + 1/2 + 1/3 + …+ 1/n  i två varianter

med while:

med for:

def harmonic(n):
    sum = 0
    i = 1
    while i <= n:
        sum += 1/i
        i += 1
    return sum
def harmonic(n):
    sum = 0
    for i in range(1, n+1):
        sum += 1/i
    return sum

Båda funktionerna har två lokala variabler (sum och i). Dessa existerar bara inuti funktionen och deras värden ”glöms” när funktionen lämnas.

Övningar#

  1. Skriv en funktion how_many(limit) som beräknar och returnerar antalet termer som behövs i den harmoniska serien för att summan ska bli större än limit.

    Svar

    Enklast att uttrycka med while:

    def how_many(limit):
        sum = 0
        n = 0
        while sum < limit:
            n += 1
            sum += 1/n
        return n
    
  2. Skriv en funktion digits(x) som returnerar antalet siffror som finns i det positiva heltalet x. Låt digits(0) vara 0. Tips: Använd heltalsdivision (//).

    Svar
    def digits(x):
        n = 0
        while x != 0:
            n += 1
            x = x//10
        return n
    
  3. Lös uppgiften ovan med hjälp av math.log10 istället.

    Svar
    import math
    
    def digits(x):
        if x == 0: # Specialhantera fallet x=0 eftersom log10(0) är odefinierat.
            return 0
        else:
            return int(math.log10(x)) + 1
    
  4. Skriv en funktion digit_sum(x) som returnerar summan av siffrorna heltalet x. Tips: Använd modulo-operatorn (%) som ger resten vid heltalsdivision.

    Svar
    def digit_sum(x):
        sum = 0
        while x != 0:
            sum += x % 10
            x = x//10
        return sum
    

Exempel: Funktion med flera parametrar#

Funktionsdefinition

Kommentarer

import math

def triangle_area(a, b, c):
    s = (a + b + c)/2
    t = s*(s-a)*(s-b)*(s-c)
    r = math.sqrt(t)
    return r

Tre parametrar och tre lokala variabler.

Exempel på användning

Värde

triangle_area(3, 4, 5)

6

triangle_area(1, 1, math.sqrt(2))

0.49999999999999983

Övningar#

  1. Vad händer om man anropar funktionen med värden som inte kan utgöra sidor i samma triangel (t.ex. triangle_area(1, 2, 10))? Om den lokala variabeln t får ett negativt värde så innebär detta att de inmatade sidorna inte kan utgöra en triangel. Modifiera funktionen så att man får en bättre felutskrift i detta fall!

    Svar
    def triangle_area(a, b, c):
        s = (a + b + c)/2
        t = s*(s-a)*(s-b)*(s-c)
        if t < 0:
            print("Can't form a triangle of: ", a, b, c)
            return None
        r = math.sqrt(t)
        return r
    

    Här har vi valt att returnera None om sidorna inte kan formera en triangel. Det finns bättre sätt att rapportera fel men det kommer vi ta upp senare.

  2. Finns det några andra värden på parametrarna som koden bör se upp med?

    Svar

    Funktionen borde kontrollera att alla parametrarna är icke-negativa.

    if a < 0 or b < 0 or c < 0:
        print('Illegal arguments to triangle_area:', a, b, c)
    

Exempel: Funktion med parameter av listtyp#

Funktionsdefinition

Kommentar

def square_sum(a_list):
    result = 0
    for x in a_list:
        result += x*x
    return result

Kom ihåg det smidiga sättet att iterera över innehållet i en lista!

Exempel på användning

Värde

square_sum([1, 2, 3, 4])

30

square_sum([23, 18, 57])

4102

square_sum([])

0

Funktioner utan returvärde#

Funktioner som inte ska returnera något särskilt värde behöver inte ha någon return-sats. Sådana funktioner får ändå automatiskt returvärdet None.

Exempel: Hälsningsfunktion#

def greet(name, course):
    print(f'Välkommen till {course}, {name}!')

I detta exempel får funktionen två parametrar som förutsätts vara av sträng-typ. Funktionen sätter ihop ett välkomstmeddelande. Funktionskroppen består alltså av en enda sats.

Exempel på användning:

Kod

Utskrift

greet('Ola', 'världen')
greet('Anna', 'världen')
Välkommen till världen, Ola!
Välkommen till världen, Anna!
kurs = 'Prog I'
namn = ['Eva', 'Gun', 'Åke']
for n in namn:
    greet(n, kurs)
Välkommen till Prog I, Eva!
Välkommen till Prog I, Gun!
Välkommen till Prog I, Åke!
k1 = "Prog1"
k2 = "BV1"
greet('Eva', k1 + ' och ' + k2)
Välkommen till Prog1 och BV1, Eva!

Returvärden#

Funktioner kan returnera vilka typer av värden som helst (int, float, bool, listor, strängar, paddor, …)

Exempel: En funktion som undersöker om ett givet tal är ett primtal#

Funktionen is_prime(n) ska returnera True om n är ett primtal, dvs. bara delbart med sig själv eller 1.

En enkel algoritm är att kontrollera om någon rest vid division av \(n\) med alla tal som ligger i intervallet \([2, n-1]\) är 0. I så fall är \(n\) inte ett primtal.

Det räcker dock att kontrollera talen upp till och med \(\sqrt{n}\) (om \(n = pq\) kan inte både \(p\) och \(q\) vara större än \(\sqrt{n}\)).

def is_prime(n):
    limit = int(n**0.5)
    i = 2
    for i in range(2, limit+1):
        if n % i == 0:
            return False
    return True

Observera:

  • **0.5 ger roten ur (det hade också gått att använda `math.sqrt)

  • Två return-satser!

    Så fort man hittar en rest som är 0 vet man att det inte kan vara ett primtal och kan direkt lämna funktionen med False som värde. Kom ihåg att funktionen lämnas så fort en return-sats exekveras. Om man kommer ur loopen så hittades ingen division som gick jämnt ut - alltså är n ett primtal och funktionen ska returnera True.

Övning#

  1. Skriv funktionen is_twin_prime(n) som returnerar True om både n och n + 2 är primtal.

    Svar
    def is_twin_prime(n):
        return is_prime(n) and is_prime(n+2)
    

Exempel: squares som returnerar en lista#

def squares(a_list):
    res = []
    for x in a_list:
        res.append(x*x)
    return res

squares([1, 2, 3, 4]) kommer att returnera [1, 4, 9, 16]

Anmärkning: I lektionen om listor kommer vi visa på ett enklare sätt att lösa detta problem.

Funktioner med flera returvärden#

En return-sats kan innehålla flera värden åtskiljda av kommatecken.

Exempel: En funktion som löser andragradsekvationen \(x^2 + px + q = 0\) kan skrivas så här (se övning i lektion 2):

def quad_equation(p, q):
    discriminant = p*p - 4*q
    if discriminant >= 0:
        d = math.sqrt(discriminant)
        x1 = (-p + d)/2.0
        x2 = (-p - d)/2.0
        return x1, x2
    else:
        return None

Observera: Två kommaseparerade uttryck på return-satsen. Returvärdet bli då en tupel. En tupel är ungefär som en lista men med en hel del begränsningar. För att avläsa värden i en tupel använder man index-operatorn [ ].

Exempelkod

Utskrift

Kommentar

r = quad_equation(-3, 2)
print(r)

(2.0, 1.0)

Resultatet blir en tupel.

x1, x2 = quad_equation(-3, 2)
print(x1, x2)

2.0 1.0

Tupeln packas automatiskt upp.

r = quad_equation(1, 1)
print(r)
None

Komplexa rötter. Funktionen returnerar i detta fall None.

x1, x2 = quad_equation(1, 1)
print(x1, x2)
TypeError: cannot unpack non-iterable NoneType object

Går inte att göra om inte en return-sats med två värden utförts.

Man kan undersöka om returvärdet är None innan något skrivs ut:

r = quad_equation(a, b)
if r == None:
    print('Komplexa rötter')
else:
    print('x1: ', r[0])
    print('x2: ', r[1])

Undantag#

Vi ska nu diskutera vad en funktion bör göra om den får en omöjlig uppgift. Som redan påpekats så borde t.ex. funktionen triangle_area kontrollera att de tre längderna som den fick som parametrar faktiskt kan forma en triangel. För detta krävs dels att alla parametrarna är positiva och dels att värdet som skickas till sqrt är positivt.

Det är betydligt bättre att låta funktionen kasta ett undantag än att ge en felutskrift. Exempel:

def triangle_area(a, b, c):
    s = (a + b + c)/2
    t = s*(s-a)*(s-b)*(s-c)
    if a <= 0 or b <= 0 or c <= 0 or t <= 0:
        raise ValueError('Illegal parameter values in triangle_area')
    r = math.sqrt(t)
    return r

Om vi nu anropar t.ex. med uttrycket triangle_area(-1, 50, 5) kommer programmet avbrytas med följande utskrift:


Traceback (most recent call last):
  File "test.py", line 14, in <module>
    print(triangle_area(-1, 50, 5))
  File "test.py", line 8, in triangle_area
    raise ValueError('Illegal parameter values in triangle_area')
ValueError: Illegal parameter values in triangle_area

Om parametervärdena inte kan forma en triangel skapar uttrycket raise ValueError(...) ett så kallat undantag (eng. exception) av typen ValueError. Detta är en fördefinierad undantagstyp som passar någorlunda bra här. (Det vore bättre att definiera en egen undantagstyp men det tar vi inte upp här.)

Varför är då detta bättre än att göra en felutskrift med print? Utskriften ser ju (kanske) mer kryptisk ut och programmet avbryts - ville vi verkligen det?

Problemet är att funktionen triangle_area visserligen kan upptäcka fel men den har ingen aning hur felet ska åtgärdas. Genom att kasta ett undantag överlåts den hanteringen till anroparen. Om den anropande koden inte gör något så avbryts programmet, men den kan också välja att ta hand om felet med konstruktionen try - except. Exempel:

while True:
    print('Give side lengths')
    a = float(input('First: '))
    b = float(input('Second: '))
    c = float(input('Third: '))
    try:
        print('The area is', triangle_area(a, b, c))
        break
    except ValueError:
        print('\n*** Bad arguments to triangle_area!')
        print('Try again!')

Exempel på körning:

Give side lengths
First: 1 
Second: 100
Third: 2

*** Bad arguments to triangle_area!
Try again!
Give side lengths
First: -3
Second: 4
Third: 5

*** Bad arguments to triangle_area!
Try again!
Give side lengths
First: 3
Second: 4
Third: 5
The area is 6.0

Om ett undantag av typen ValueError inträffar i det indenterade blocket efter try så kommer blocket efter except utföras och därefter upprepas satserna i while-loopen. Om inget ValueError-undantag skapas så kommer loopen avbrytas när break-satsen utförs.

Fördelen är alltså att triangle_area bara rapporterar felet och låter anroparen avgöra vad som ska göras.

Övning#

Antag att vi verkligen inte vill tillåta komplexa rötter i funktionen quad_equation. Modifiera koden så att den kastar ett undantag om den upptäcker sådana.