Göm menyn

Hur man programmerar

Programutvecklingsprocessen

När man skriver ett program är det viktigt att först tänker igenom vad det ska göra. Om man inte gör det blir koden ofta en ohanterlig röra. Här är en bild som bra beskriver hur du bör dela upp din arbetsprocess. Det är inte alltid viktigt att följa denna modell precist, men ha den alltid i åtanke när du skriver dina program.

beskrivning av programutveckligsprocessen

Exempel

Vi ska nu kolla på ett exempel som visar de olika stegen.

Analys

Vi ska förstå vilket problem som ska lösas. Därför pratar vi med den som är intresserad av programmet och ställer frågor för att få reda på vad man egentligen vill.

Pelle går en kurs i EcoDriving för att bli bättre på att köra sin bil mer bränslesnålt. Han har tyvärr en gammal bil som inte visar hur mycket bensin den drar, men han vill ändå kunna hålla koll på bränsleförbrukningen. För att göra det hela lite mer spännande vill han ha ett Python-program som hjälper honom.

Specifikation

Vi måste nu tänka genom problemet i lite mer detalj. Vad har vi till exempel för in- och utdata? Vilken funktionalitet behöver finnas? Detta säger inget om hur vi åstadkommer det hela, bara vad vi vill ha.

Pelle vill ha ett enkelt program som tar en körd sträcka (i km) och hur mycket han har tankat (i liter). Programmet ska sedan räkna ut bränsleförbrukningen och skriva ut den.

Design

Nu är det till slut dags att tänka genom hur vi ska lösa problemet. Vi behöver fundera på hur funktionaliteten ska delas upp och hur olika delar av programmet ska prata med varandra. I små program som detta kan processen vara rätt så enkel och kanske till och med se uppenbar ut. I större program behöver man lägga ner större möda på att även t.ex. definiera hur information ska lagras internt.

Programmet kommer att bestå av tre delar:

  • Inmatning av information från användaren.
  • Uträkning av förbrukning enligt formeln förbrukning = bensinmängd/sträcka.
  • Utskrift av resultatet.

Implementation

Dags att skriva kod! Återigen blir det ett väldigt enkelt exempel, nu med bara en enda funktion.

def fuel_efficiency():
    """ Lets the user input travel data and calculates the fuel efficiency """
    dist = eval(input("Distance (km): "))
    cons = eval(input("Consumption (l): "))
    eff = cons/(dist/10)
    print("Your car consumes ", eff, " liters per ten kilometers")

Testning

Körexempel:

>>> fuel_efficiency()
Distance (km): 800
Consumption (l): 52
Your car consumes 0.65 liters per ten kilometers.

Hur kan vi tillämpa detta?

Tänk efter innan du börjar skriva kod!

Rita upp hur du tänker att programmet ska fungera innan du börjar. Penna och papper är bra hjälpmedel för detta.

Använd pseudokod

När vi funderar ut precis hur programmet ska fungera, kan vi behöva ett mellansteg mellan att (1) skriva allt i vanlig text, och (2) skriva en fullständig implementation med 100% korrekt körbar programkod. Alternativ 1 kan ju vara alltför vagt, medan alternativ 2 kräver att vi verkligen har alla små detaljer helt klara för oss. Då kan vi använda så kallad pseudokod som ett mellansteg.

I detta mellansteg kan man ta sin beskrivning i vanligt naturligt språk och börja göra den mer formell, men utan att gå ner i precis alla detaljer. Som exempel kan vi ta djupet-först-sökning -- ett begrepp som ni egentligen inte behöver kunna just nu, men som är lagom komplicerat för att vi ska kunna se skillnaden mellan vanlig text, pseudokod, och äkta kod.

En textbeskrivning kan se ut så här (från Wikipedia):

Depth-first search (DFS) is an algorithm for traversing or searching tree or graph data structures. The algorithm starts at the root node (selecting some arbitrary node as the root node in the case of a graph) and explores as far as possible along each branch before backtracking. 

Som vi ser är det här ganska vagt och uttrycker bara generella idéer. Vi kan försöka göra det tydligare genom att fortsätta beskriva algoritmen i ren text -- eller så kan vi låna in begrepp från programmering och beskriva allt i pseudokod istället (också Wikipedia):

1  procedure DFS(G,v):
2      label v as discovered
3      for all directed edges from v to w that are in G.adjacentEdges(v) do
4          if vertex w is not labeled as discovered then
5              recursively call DFS(G,w)

Detta är helt klart ett mer precist uttryck för hur algoritmen ska fungera. Samtidigt har vi inte gått så långt som att beskriva exakt hur etiketter som "discovered" ska lagras, eller hur man tar alla bågar (edges) och tar fram bara de som är riktade (directed) och går från v till w. Istället har vi skrivit saker som:

  • label ... as discovered
  • for all directed edges from v to w that are in...
  • if ... is not labeled as discovered ...

Det finns inget programspråk som förstår detta och kan utföra pseudokoden, men det är inte dess syfte. Syftet är istället att:

  • Innan implementationen är färdig: Hitta ett mellansteg där vi kan se om vår tänkta algoritm verkar stämma, utan att vi behöver gå ända ner till en fullständig implementation (som kanske har 10 eller 100 gånger så mångar rader kod). För att uppnå detta syfte måste vi skriva pseudokod på en nivå där vi förstår exakt vad som ska göras. Det gjorde vi kanske inte från beskrivningen på ren engelska, men pseudokoden kan ge lagom mycket detaljer för att vi ska upptäcka eventuella tankemissar och felsteg innan vi lägger ner för mycket tid på att implementera.

  • Efter att implementationen är färdig och testad: Ha ett sätt att beskriva algoritmen för andra, utan att binda sig till ett specifikt programmeringsspråk.

Det finns ingen definierad syntax för pseudokod, utan man skriver ner utformningen av det tilltänkta programmet på en form man själv är bekväm med.

Experimentera och lär känna dig själv!

Det är oftast lättare att börja med ett annat, lite enklare problem. Testa hela tiden, och ändra om något inte fungerar.

Divide and Conquer

Divide and conquer är en problemlösningsmetod som går ut på att dela upp ett större problem i mindre delar. Dessa delar är oftast enklare problem att lösa än att försöka sig på att lösa hela problemet direkt. Lösningarna till de mindre delarna kan sedan kombineras till en lösning på det större problemet. Tänk på följande när du använder divide and conquer:

  1. Dela upp lösningen i flera funktioner så att varje funktion gör en sak och får ett vettigt namn. Använd terminologin för problemet. Det gör det enkelt att koppla ihop en funktion med en viss funktionalitet.
  2. Gör varje funktion lagom stor. Det ska vara lätt att överblicka och testa varje funktion var för sig, men dela inte upp lösningen i för små delar. Gör man det kan det bli svårt att skapa sig en överblick, och koden kan bli onödigt ineffektiv.
  3. Skriv funktioner som är generella och går att återanvända i andra projekt. Du tjänar oftast på att lösa lite större problem än du faktiskt har.

Exempel: Beräkning av perfekta tal

Vi säger att ett heltal k delar ett tal n om det finns ett heltal a sådant att a * k = n. Lite enklare kan man säga att k går jämnt upp i n. Vi säger att n är ett perfekt tal om summan av alla delarna är lika med n. Det finns inte särskilt många perfekta tal.

De tre lägsta är:

  • 6 (med delarna 1, 2 och 3)
  • 28 (med delarna 1, 2, 4, 7 och 14)
  • 496 (med delarna 1, 2, 4, 8, 16, 31, 62, 124 och 248)

Vi vill ha en Python-funktion perfect som tar reda på om ett heltal är perfekt eller inte. Hur kan vi dela upp problemet i mindre bitar?

Den här bilden illustrerar ett sätt att dela upp problemet.

Perfekta tal uppdelat i mindre delar

Implementation

Nu när vi har identifierat hur vi kan dela upp problemet är det dags att implementera det.

def divides(k, n):
    """ Checks if k divides n """
    return n % k == 0

def dividers(n):
    """ Return a list of all dividers of n """
    res = []
    for i in range(1, n//2+1)
        if divides(i, n):
            res = res + [i]
    return result

Testa koden:

>>> divides(3, 24)
True
>>> divides(2, 25)
False
>>> dividers(10)
[1, 2, 5]

När vi vet att koden fungerar som den ska kan vi fortsätta med resten av implementationen.

def sum_numbers(seq):
    """ Returns the sum of a list of numbers """
    if not seq:
        return 0
    else:
        return seq[0] + sum_numbers(seq[1:])

def perfect(n):
    """ Checks if n is a perfect number """
    return n == sum_numbers(dividers(n))
>>> sum_numbers([1, 2, 3, 4])
10
>>> sum_numbers(dividers(10))
8
>>> perfect(10)
False
>>> perfect(6)
True

Sidansvarig: Peter Dalenius
Senast uppdaterad: 2021-12-03