Kapittel 14 Andre hovedprosjekt


Vi har nå gått gjennom all teorien i hele boken. Det er jaggu på tide med et skikkelig prosjekt for å runde av del 2 av boken.

I dette hovedprosjektet skal vi lage en huskeliste som hjelper oss med å holde orden på oppgavene våre. For å gjøre det litt morsommere enn en vanlig liste, skal vi bygge inn spill-logikk. Du skal vinne poeng og klatre i nivåer når du fullfører oppgaver.

Vi skal bruke store deler av det du har lært gjennom boken. Vi kommer blant annet til å skrive til filer, bruke sammensatte datatyper, lage klasser og objekter, og benytte oss av funksjoner av høyere orden. La oss hoppe i gang!

14.1 Introduksjon – Konseptet bak vår spillifiserte huskeliste

Før vi skriver vår første linje med kode, bør vi forstå situasjonen vi ønsker å representere. Hva er det egentlig vi skal lage? En god huskeliste må dekke fire grunnleggende behov som vi i programmeringsverdenen kaller CRUD. Dette er et akronym for de fire operasjonene man gjør på data i nesten alle dataprogrammer:

  • Create (Opprette): Evnen til å legge til nye oppgaver i huskelisten.

  • Read (Lese): Evnen til å vise listen over oppgaver.

  • Update (Oppdatere): Evnen til å endre en oppgave, for eksempel markere den som fullført.

  • Delete (Slette): Evnen til å fjerne en oppgave du ikke lenger trenger.

Slik skal programmet henge sammen: Programmet vårt vil bestå av en evig løkke (motoren) som venter på kommandoer fra deg. Når du legger til en oppgave, skaper vi et objekt av en klasse. Dette objektet blir lagret i en liste, som igjen blir skrevet til en JSON-fil (lagringen). For å gjøre det hele gøy, legger vi på et lag med spill-logikk som regner ut poeng og hvilket nivå du har oppnådd.

14.1.1 Ryddighet i systemet

Så langt i boken har vi stort sett skrevet all kode i én enkelt Python-fil. Når prosjektene blir større, blir dette fort uoversiktlig. Derfor skal vi dele programmet vårt opp i tre separate Python-filer:

  • kjerne.py: Her definerer vi klassene våre. Dette er oppskriften på hvordan en oppgave og selve huskelisten skal se ut og oppføre seg. Her vil vi også legge grunnleggende funksjonalitet for CRUD-operasjonene som nevnt ovenfor, i tillegg til utregning av poengsum og nivå.

  • tjenester.py: Her skriver vi logikken som har med eksterne filer å gjøre. Dette betyr å lagre og laste huskelisten fra fil slik at programmet ikke glemmer alt etter at det har blitt avsluttet.

  • main.py: Dette er selve motoren som starter programmet, viser huskelisten, og snakker med deg i terminalen.

Dette kalles modularisering140, og det gjør det lettere å finne frem i koden og rette feil. Merk deg oppgavefordelingen mellom kjerne.py og tjenester.py: Siden alt som har med filhåndtering ligger i tjenester.py er det ikke nødvendig for klassene i kjerne.py å kommunisere med omverdenen. Da slipper vi at den grunnleggende funksjonaliteten til handlelisten vår er for tett koblet opp mot omverdenen utenfor programmet vårt. Dette er en standard måte å begrense kompleksitet på i større programmer.

I tillegg skal vi ha filen oppgaver.json. Dette er selve lageret vårt hvor alle oppgavene og poengene dine blir liggende trygt lagret, selv når du skrur av maskinen.

14.2 Konstruering av klassene

Det aller første vi må gjøre er å definere hvordan dataene våre skal se ut. Som vi lærte i kapittel 11, er klasser ypperlige til å gruppere relatert informasjon og funksjonalitet. Opprett filen kjerne.py. Denne filen skal inneholde oppskriften på hva en oppgave og en huskeliste består av.

14.2.1 Klassen Oppgave

Hver oppgave er mer enn bare en tekststreng. En oppgave i huskelisten vår skal inneholde disse attributtene:

  • .tittel: En streng med navnet på oppgaven.

  • .poeng: Et heltall som gir hvor mange poeng oppgaven er verdt.

  • .er_fullført: En sannhetsverdi som sier om oppgaven er fullført eller ikke.

  • .dato_opprettet: Datoen for når oppgaven ble opprettet.

  • .dato_fullført: Datoen for når oppgaven ble utført.

Da er vi klar til å lage klassen Oppgave. Åpne filen kjerne.py og skriv inn følgende:

from datetime import date


class Oppgave:
    """Representerer en enkelt oppgave i huskelisten."""

    def __init__(self, tittel, poeng):
        self.tittel = tittel
        self.poeng = poeng
        self.er_fullført = False
        self.dato_opprettet = date.today().isoformat()
        self.dato_fullført = None

Som du kan se, bruker vi objektet date fra standardbiblioteket datetime for å legge til dagens dato til attributten .dato_opprettet.

La oss implementere dundermetoden __str__() som vi lærte i seksjon 11.3 slik at oppgaver kan skrives ut til terminalen på en lesbar måte:

from datetime import date


class Oppgave:
    """Representerer en enkelt oppgave i huskelisten."""

    def __init__(self, tittel, poeng):
        self.tittel = tittel
        self.poeng = poeng
        self.er_fullført = False
        self.dato_opprettet = date.today().isoformat()
        self.dato_fullført = None

    def __str__(self):
        status = '[X]' if self.er_fullført else '[ ]'
        return f'{status} {self.tittel} - {self.poeng} poeng'

La oss nå hoppe over til å lage klassen Huskeliste. Vi skal senere komme tilbake til klassen Oppgave for å legge til mer funksjonalitet i form av metoder.

14.2.2 Klassen Huskeliste

Selv om vi kunne lagret alle oppgavene i en vanlig liste, er det enda bedre å lage en egen klasse som administrerer disse objektene. Dette gjør det mye enklere å håndtere poengsummen og nivåene til brukeren på ett sted. Bla ned til under klassen Oppgave i filen kjerne.py og opprett følgende klasse:

class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

Her er tanken at et objekt av klassen Huskeliste skal via attributten .oppgaver holde en liste over objekter av klassen Oppgave. I tillegg har objekter av klassen Huskeliste attributten .total_poeng som legger sammen den totale mengden poeng som har blitt oppnådd ved å fullføre oppgaver.

En huskeliste som alltid er tom er ikke særlig nyttig. Den første funksjonaliteten vi trenger handler om å legge til oppgaver til huskelisten:

class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def legg_til_oppgave(self, ny_oppgave):
        """Legger til en ny oppgave til huskelisten."""
        self.oppgaver.append(ny_oppgave)

La oss også lage en .__str__() dundermetode for klassen Huskeliste slik at det kommer klart frem hva en huskeliste inneholder når vi skriver den ut til terminalen:

class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def legg_til_oppgave(self, ny_oppgave):
        """Legger til en ny oppgave til huskelisten."""
        self.oppgaver.append(ny_oppgave)

    def __str__(self):
        overskrift = f'--- STATUS: Total poengsum {self.total_poeng} ---'

        if not self.oppgaver:
            return f'{overskrift}\nHuskelisten er tom. Legg til noe å gjøre!'

        linjer = [overskrift]
        for i, oppgave in enumerate(self.oppgaver):
            linjer.append(f'{i + 1}. {oppgave}')

        return '\n'.join(linjer)

Vi kan nå teste ut opprettelsen av en del oppgaver i en huskeliste. Her kan du se testkode som demonstrerer funksjonaliteten så langt:

# 1. Vi oppretter en huskeliste
min_huskeliste = Huskeliste()

# 2. Vi oppretter noen oppgaver
oppgave1 = Oppgave('Vaske sykkelen', 50)
oppgave2 = Oppgave('Jobbe med Python', 100)

# 3. Vi legger oppgavene inn i huskelisten
min_huskeliste.legg_til_oppgave(oppgave1)
min_huskeliste.legg_til_oppgave(oppgave2)

# 4. Vi printer ut statusen så langt
print(min_huskeliste)

Koden over kan du legge til nederst i filen kjerne.py og så kjøre filen. Da bør du få følgende utskrift til terminalen:

--- STATUS: Total poengsum 0 ---
1. [ ] Vaske sykkelen - 50 poeng
2. [ ] Jobbe med Python - 100 poeng

Etter at denne innledende testen kjører som forventet så kan du slette testkoden før du går videre. Koden vår i kjerne.py ser slik ut så langt:

from datetime import date


class Oppgave:
    """Representerer en enkelt oppgave i huskelisten."""

    def __init__(self, tittel, poeng):
        self.tittel = tittel
        self.poeng = poeng
        self.er_fullført = False
        self.dato_opprettet = date.today().isoformat()
        self.dato_fullført = None

    def __str__(self):
        status = '[X]' if self.er_fullført else '[ ]'
        return f'{status} {self.tittel} - {self.poeng} poeng'


class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def legg_til_oppgave(self, ny_oppgave):
        """Legger til en ny oppgave til huskelisten."""
        self.oppgaver.append(ny_oppgave)

    def __str__(self):
        overskrift = f'--- STATUS: Total poengsum {self.total_poeng} ---'

        if not self.oppgaver:
            return f'{overskrift}\nHuskelisten er tom. Legg til noe å gjøre!'

        linjer = [overskrift]
        for i, oppgave in enumerate(self.oppgaver):
            linjer.append(f'{i + 1}. {oppgave}')

        return '\n'.join(linjer)

14.3 Grunnleggende logikk

Det første vi må kunne gjøre med huskelisten og oppgavene er de resterende CRUD-operasjonene vi beskrev i seksjon 14.1.

Vi har allerede skrevet metoden .legg_til_oppgave() i klassen Huskeliste for å legge til oppgaver (Create). Dundermetoden .__str__() lar oss vise hva som er i en handleliste på en enkel måte, så dette dekker lesning (Read). Det gjenstår oppdatering (Update) og sletting (Delete).

14.3.1 Sletting av oppgaver

La oss begynne med sletting siden dette er relativt enkelt:

class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def slett_oppgave(self, oppgave):
        """Sletter en oppgave fra huskelisten."""
        if oppgave in self.oppgaver:
            self.oppgaver.remove(oppgave)
            return oppgave
        else:
            print(f'Oppgaven {oppgave.tittel} eksisterer ikke!')
            
    # -- De andre metodene er fjernet for leselighet --

Som du kan se sjekker .slett_oppgave() om oppgaven som skal slettes faktisk er med i huskelisten. Hvis den finner et treff så blir oppgaven fjernet og vi returnerer også oppgaven. Finner vi ikke et treff så skriver vi heller ut en enkel beskjed.

Hva skjer dersom en huskeliste har den samme oppgaven to ganger? Da vil vi ved å bruke metoden .slett_oppgave() bare fjerne den første av dem. Her er det flere måter å løse dette på, men den enkleste er å ikke tillate duplikater til å begynne med. Dermed kan vi endre den eksisterende metoden .legg_til_oppgave() slik:

class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def legg_til_oppgave(self, ny_oppgave):
        """Legger til en ny oppgave til huskelisten."""
        if ny_oppgave.tittel not in [oppgave.tittel for oppgave in self.oppgaver]:
            self.oppgaver.append(ny_oppgave)
        else:
            print('Oppgaven eksisterer allerede i huskelisten.')
        
    # -- De andre metodene er fjernet for leselighet --

Med dette er vi ferdig med sletting (Delete) som funksjonalitet. La oss nå ta for oss oppdatering (Update).

14.3.2 Oppdatering av oppgaver

For å oppdatere en enkelt oppgave i etterkant kan vi alltids endre på oppgavens attributter. Likevel er det greit å ha funksjonalitet for å fullføre en oppgave siden det å fullføre en oppgave har ringvirkninger:

  • Når du setter en oppgave som fullført må også attributten .dato_fullført bli endret. Det gir ikke logisk mening å ha en oppgave som har verdien True for attributten .er_fullført og samtidig har verdien None for attributten .dato_fullført.

  • Når du setter en oppgave som fullført må den totale poengsummen som er registrert i attributten .total_poeng til huskelisten bli oppdatert.

Dersom en bruker må sette attributten .er_fullført til verdien True manuelt selv når en oppgave er løst, er det fort lett å glemme de to punktene ovenfor. Derfor oppretter vi egen funksjonalitet for å fullføre en oppgave.

Før vi markerer en oppgave som ferdig i klassen Huskeliste, må selve Oppgave-objektet vite hvordan det skal oppdatere sin egen status. Vi hopper derfor et lite øyeblikk tilbake til klassen Oppgave for å legge til en metode som håndterer fullføring. Når en oppgave markeres som fullført, må vi endre attributten .er_fullført til verdien True og registrere datoen det skjedde. Legg til metoden .marker_fullført() i klassen Oppgave slik:

from datetime import date


class Oppgave:
    """Representerer en enkelt oppgave i huskelisten."""

    def __init__(self, tittel, poeng):
        self.tittel = tittel
        self.poeng = poeng
        self.er_fullført = False
        self.dato_opprettet = date.today().isoformat()
        self.dato_fullført = None

    def marker_fullført(self):
        """Markerer oppgaven som fullført og setter dato for fullføring."""
        if not self.er_fullført:
            self.er_fullført = True
            self.dato_fullført = date.today().isoformat()
            return True
        return False

    def __str__(self):
        status = '[X]' if self.er_fullført else '[ ]'
        return f'{status} {self.tittel} - {self.poeng} poeng'

Ved å returnere True eller False kan vi gi beskjed til resten av programmet om oppgaven faktisk ble endret. Hvis oppgaven allerede var fullført fra før av returnerer vi verdien False for å indikere dette.

Nå kan vi gå tilbake til klassen Huskeliste. Her trenger vi en metode som finner riktig oppgave og sørger for at poengsummen til brukeren i attributten .total_poeng blir oppdatert. Dette er selve kjernen i spill-logikken vår. Legg til metoden .fullfør_oppgave() i klassen Huskeliste:

class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def fullfør_oppgave(self, oppgave):
        """Finner en oppgave basert på tittel og tildeler poeng."""
        if oppgave in self.oppgaver and oppgave.marker_fullført():
            self.total_poeng += oppgave.poeng
            print(f'Fullført {oppgave.tittel} + {oppgave.poeng} poeng.')
            return True
        return False

    # -- De andre metodene er fjernet for leselighet --

Dette er et godt eksempel på hvordan objekter samarbeider. Huskelisten ber oppgaven om å markere seg selv som ferdig. Dersom dette går bra, henter huskelisten poengverdien fra oppgaven og legger den til i totalen i attributten .total_poeng. På denne måten trenger ikke klassen Huskeliste å forholde seg til hvordan klassen Oppgave markerer en oppgave som fullført. På den andre siden trenger heller ikke klassen Oppgave å ha noe kunnskap om den totale poengsummen siden dette er representert i klassen Huskeliste. En slik fordeling av ansvar gir oss et elegant grensesnitt mellom de to klassene.

14.3.3 Sortering av oppgaver

En god huskeliste må kunne sorteres på forskjellige måter. Her er to situasjoner som jeg tenker er umiddelbart nyttige:

  • Sortering av oppgavene alfabetisk.

  • Sortering av oppgavene etter poeng.

Her kunne vi skrevet to forskjellige metoder, men jeg tenker det holder med en metode .sorter_oppgaver() som sorterer oppgaver. Metoden .sorter_oppgaver() kan konfigureres til å sortere basert på forskjellige verdier vi gir den i en parameter slik:

class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""
    
    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0
      
    def sorter_oppgaver(self, kriterium='alfabetisk'):
        """Sorterer oppgavene basert på et kriterium."""
        if kriterium == 'alfabetisk':
            self.oppgaver.sort(
                key=lambda oppgave: oppgave.tittel
            )
        elif kriterium == 'poengsum':
            self.oppgaver.sort(
                key=lambda oppgave: oppgave.poeng,
                reverse=True
            )
        else:
            print('Dette er ikke et gyldig kriterium.')

    # -- De andre metodene er fjernet for leselighet --

Siden metoden .sort() sorterer fra minste til største tall har jeg lagt på reverse=True når vi sorterer etter poengsum. Da blir oppgavene med størst poengsum vist først, som er mest sannsynlig det brukeren ønsker å se. Dersom ingen av de godkjente kriteriene blir gitt via parameteren kriterium vil metoden .sorter_oppgaver() skrive ut en enkel beskjed til terminalen.

I metoden .sorter_oppgaver() har vi lagt opp til at du kan legge til nye kriterier for sortering. Kanskje du for eksempel ønsker å sortere oppgavene slik at alle oppgavene som er fullført kommer på toppen av huskelisten? Jeg overlater det til deg å legge til flere kriterier om du ønsker.

I neste seksjon skal vi starte på filen tjenester.py for å skrive logikken for permanent lagring og lasting av huskelister. Koden vi har skrevet i filen kjerne.py ser per nå slik ut:

from datetime import date


class Oppgave:
    """Representerer en enkelt oppgave i huskelisten."""

    def __init__(self, tittel, poeng):
        self.tittel = tittel
        self.poeng = poeng
        self.er_fullført = False
        self.dato_opprettet = date.today().isoformat()
        self.dato_fullført = None

    def marker_fullført(self):
        """Markerer oppgaven som fullført og setter dato for fullføring."""
        if not self.er_fullført:
            self.er_fullført = True
            self.dato_fullført = date.today().isoformat()
            return True
        return False

    def __str__(self):
        status = '[X]' if self.er_fullført else '[ ]'
        return f'{status} {self.tittel} - {self.poeng} poeng'


class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def legg_til_oppgave(self, ny_oppgave):
        """Legger til en ny oppgave til huskelisten."""
        if ny_oppgave.tittel not in [oppgave.tittel for oppgave in self.oppgaver]:
            self.oppgaver.append(ny_oppgave)
        else:
            print('Oppgaven eksisterer allerede i huskelisten.')

    def fullfør_oppgave(self, oppgave):
        """Finner en oppgave basert på tittel og tildeler poeng."""
        if oppgave in self.oppgaver and oppgave.marker_fullført():
            self.total_poeng += oppgave.poeng
            print(f'Fullført {oppgave.tittel} + {oppgave.poeng} poeng.')
            return True
        return False

    def slett_oppgave(self, oppgave):
        """Sletter en oppgave fra huskelisten."""
        if oppgave in self.oppgaver:
            self.oppgaver.remove(oppgave)
            return oppgave
        else:
            print(f'Oppgaven {oppgave.tittel} eksisterer ikke!')

    def sorter_oppgaver(self, kriterium='alfabetisk'):
        """Sorterer oppgavene basert på et kriterium."""
        if kriterium == 'alfabetisk':
            self.oppgaver.sort(
                key=lambda oppgave: oppgave.tittel
            )
        elif kriterium == 'poengsum':
            self.oppgaver.sort(
                key=lambda oppgave: oppgave.poeng,
                reverse=True
            )
        else:
            print('Dette er ikke et gyldig kriterium.')

    def __str__(self):
        overskrift = f'--- STATUS: Total poengsum {self.total_poeng} ---'

        if not self.oppgaver:
            return f'{overskrift}\nHuskelisten er tom. Legg til noe å gjøre!'

        linjer = [overskrift]
        for i, oppgave in enumerate(self.oppgaver):
            linjer.append(f'{i + 1}. {oppgave}')

        return '\n'.join(linjer)

14.4 Lagring og lasting

Nå som vi har de logiske byggeklossene på plass i kjerne.py, er det på tide å flytte fokuset over til tjenester.py. Som vi nevnte i introduksjonen, er dette filen som skal fungere som bindeleddet mellom programmet vårt og den fysiske lagringsplassen på maskinen din.

Selv om vi nå kan opprette oppgaver i minnet, vil alt forsvinne så fort vi lukker terminalen. For at huskelisten skal ha noen praktisk verdi, må dataene overleve at programmet avsluttes. Til dette skal vi bruke JSON-formatet som vi lærte om i seksjon 10.5.

JSON er et rent tekstformat. Det betyr at en JSON-fil ikke forstår klassene Oppgave og Huskeliste, men bare mer standardiserte datatyper som strenger, tall, lister og ordbøker. Vi står derfor overfor to prosesser:

  • Serialisering:141 Vi må oversette objektene våre til ordbøker slik at de kan lagres som en JSON-fil.

  • Deserialisering:142 Vi må lese JSON-filen og oversette dette tilbake til objekter av klassene Oppgave og Huskeliste.

14.4.1 Lagre huskelisten til en JSON-fil

La oss starte med å skrive funksjonen som serialiserer og lagrer dataene våre. Opprett filen tjenester.py og legg til følgende kode:

import json
from kjerne import Oppgave, Huskeliste


def lagre_til_fil(huskeliste, filnavn='oppgaver.json'):
    """Konverterer huskelisten til en ordbok og lagrer den som JSON."""
    data = {
        'total_poeng': huskeliste.total_poeng,
        'oppgaver': [oppgave.__dict__ for oppgave in huskeliste.oppgaver]
    }

    with open(filnavn, 'w', encoding='utf-8') as fil:
        fil.write(json.dumps(data, ensure_ascii=False, indent=4))
    print(f'Data lagret til {filnavn}')

Vi bruker ikke klassene Oppgave og Huskeliste enda, men vi kommer til å trenge dem når vi skal laste huskelisten fra en JSON-fil.

I koden bruker vi et lurt triks, nemlig oppgave.__dict__. For et objekt A så gir A.__dict__ oss alle attributtene til objektet som en ordbok. Ved å bruke en listebeskrivelse gjør vi hele listen med objekter om til en liste med ordbøker som JSON-biblioteket forstår.

Resten av funksjonen skriver all informasjonen til JSON-filen. Her bruker vi indent=4 for å gjøre den endelige JSON-filen litt mer leselig for oss mennesker når vi åpner den. Dersom vi ikke legger dette til, vil hele JSON-filen komme på en enkelt linje. Dette er helt uproblematisk for Python å håndtere, men det er litt fint å kunne lese JSON-filen på en enkel måte. I tillegg spesifiserer vi ensure_ascii=False for at vi skal få de særnorske bokstavene riktig formatert.

Det er fint å sjekke om funksjonen lagre_til_fil() faktisk fungerer slik vi tror. Jeg anbefaler at du legger til følgende linjer med kode på slutten av filen tjenester.py:

# 1. Vi lager en huskeliste med litt innhold
min_liste = Huskeliste()

oppgave1 = Oppgave('Lære JSON i Python', 100)
oppgave2 = Oppgave('Stå på ski', 50)

min_liste.legg_til_oppgave(oppgave1)
min_liste.legg_til_oppgave(oppgave2)
min_liste.fullfør_oppgave(oppgave1)

# 2. Vi lagrer listen
lagre_til_fil(min_liste)

Hvis du kjører filen tjenester.py vil du se at koden vi har skrevet så langt gir oss det vi forventer. Vi får opprettet en fil oppgaver.json som har følgende innhold:

{
    "total_poeng": 100,
    "oppgaver": [
        {
            "tittel": "Lære JSON i Python",
            "poeng": 100,
            "er_fullført": true,
            "dato_opprettet": "2026-02-27",
            "dato_fullført": "2026-02-27"
        },
        {
            "tittel": "Stå på ski",
            "poeng": 50,
            "er_fullført": false,
            "dato_opprettet": "2026-02-27",
            "dato_fullført": null
        }
    ]
}

Du kan fjerne testkoden fra filen tjenester.py før vi går videre til å skrive en funksjon som laster huskelisten fra en JSON-fil.

14.4.2 Laste huskelisten fra en JSON-fil

Når vi skal laste inn igjen, er det ikke nok å bare bruke funksjonen json.load(). Det ville gitt oss en liste med vanlige ordbøker, og vi ville ikke kunne kalt metoder som .marker_fullført() på dem. Vi må derfor deserialisere JSON-filen slik at vi kan gjenskape objektene av klassene Oppgave og Handleliste. Legg til denne funksjonen i tjenester.py:

import json    
from kjerne import Oppgave, Huskeliste


def last_fra_fil(filnavn='oppgaver.json'):
    """Leser en JSON-fil og gjenskaper Huskeliste- og Oppgave-objekter."""
    try:
        with open(filnavn, 'r', encoding='utf-8') as fil:
            data = json.load(fil)

        ny_huskeliste = Huskeliste()
        ny_huskeliste.total_poeng = data.get('total_poeng', 0)

        for informasjon in data.get('oppgaver', []):
            oppgave = Oppgave(informasjon['tittel'], informasjon['poeng'])
            oppgave.er_fullført = informasjon['er_fullført']
            oppgave.dato_opprettet = informasjon['dato_opprettet']
            oppgave.dato_fullført = informasjon['dato_fullført']
            ny_huskeliste.legg_til_oppgave(oppgave)

        return ny_huskeliste

    except FileNotFoundError:
        return Huskeliste()
      
# -- Den andre funksjonen er fjernet for leselighet --

Her ser du viktigheten av modularisering. Vi importerer klassene fra kjerne.py og bruker dem som former for å støpe dataene tilbake til sin opprinnelige struktur. Ved å bruke nøkkelordene try og except sørger vi også for at programmet ikke krasjer når det ikke eksisterer en JSON-fil. Nå kan vi nemlig også kalle funksjonen last_fra_fil() når vi starter programmet siden den vil bare returnere en tom huskeliste dersom det ikke eksisterer en fil. Dette er jo akkurat det vi ønsker når det ikke finnes en tidligere opprettet huskeliste!

Den siste funksjonaliteten vi trenger i tjenester.py er opprettelsen og lagringen av en visuell analyse som viser hvordan du går opp i poeng og nivåer. Vi venter med dette til vi har laget ferdig brukergrensesnittet i filen main.py og utvidet logikken for hvordan poeng skal omdannes til nivåer. Som regel er analyser noe av det siste vi legger på ettersom det krever at det meste annet er på plass allerede.

14.5 Brukergrensesnittet

Nå skal vi bygge motoren som binder alt sammen. Opprett filen main.py, som er selve brukergrensesnittet for programmet. Det er her vi tar imot beskjeder fra brukeren, kaller på metodene i klassene som ligger i filen kjerne.py, og sørger for at alt blir trygt lagret via funksjonene i filen tjenester.py.

For å få til dette skal vi bruke en uendelig løkke for å vente på beskjeder fra brukeren. Dette er ganske likt som i det første hovedprosjektet vi skrev i kapittel 7. Det første vi må gjøre er å importere verktøyene våre og sørge for at vi laster inn eksisterende data med en gang programmet starter. Åpne main.py og legg inn følgende:

from kjerne import Oppgave, Huskeliste
from tjenester import lagre_til_fil, last_fra_fil


def main():
    min_huskeliste = last_fra_fil()
    print('Velkommen til din spillifiserte huskeliste!')


# Starter programmet
if __name__ == '__main__':
    main()

Vi trenger en oversiktlig meny slik at brukeren vet hvilke valg som finnes. I første omgang holder det at brukeren kan legge til nye oppgaver, fullføre oppgaver, slette oppgaver, og avslutte programmet slik at alt blir lagret. For at brukeren skal spesifisere handlinger på oppgaver, trenger vi først å legge til hjelpemetoden .finn_oppgave() i klassen Huskeliste i filen kjerne.py:

class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def finn_oppgave(self, tittel):
        """Leter etter en oppgave med gitt tittel og returnerer objektet."""
        for oppgave in self.oppgaver:
            if oppgave.tittel.lower() == tittel.lower():
                return oppgave
    
    # -- De andre metodene er fjernet for leselighet --          

Metoden .finn_oppgave() vil la brukeren kunne spesifisere tittelen på en oppgave når en handling skal utføres. Vi bruker da .finn_oppgave() til å finne Oppgave-objektet som hører til tittelen. Vi kan nå utvide funksjonen main() med de forskjellige valgene:

from kjerne import Oppgave, Huskeliste
from tjenester import lagre_til_fil, last_fra_fil


def main():
    min_huskeliste = last_fra_fil()
    print('Velkommen til din spillifiserte huskeliste!')
    
    while True:
        print('\n--- MENY ---')
        print('1. Vis huskeliste')
        print('2. Legg til ny oppgave')
        print('3. Fullfør en oppgave')
        print('4. Slett en oppgave')
        print('5. Lagre og avslutt')
        
        valg = input('\nVelg et alternativ (1-5): ')

        if valg == '1':
            print('\n', min_huskeliste)
        elif valg == '2':
            tittel = input('Hva skal du gjøre? ')
            poeng = int(input('Hvor mange poeng er denne verdt? '))
            ny_oppgave = Oppgave(tittel, poeng)
            min_huskeliste.legg_til_oppgave(ny_oppgave)
        elif valg == '3':
            tittel = input('Hvilken oppgave har du fullført? ')
            oppgave = min_huskeliste.finn_oppgave(tittel)
            if oppgave:
                min_huskeliste.fullfør_oppgave(oppgave)
            else:
                print(f'Fant ingen oppgave med navn {tittel}.')
        elif valg == '4':
            tittel = input('Hvilken oppgave vil du slette? ')
            oppgave = min_huskeliste.finn_oppgave(tittel)
            if oppgave:
                min_huskeliste.slett_oppgave(oppgave)
            else:
                print(f'Fant ingen oppgave med navn {tittel}.')
        elif valg == '5':
            lagre_til_fil(min_huskeliste)
            print('Programmet avsluttes. Lykke til videre!')
            break
        else:
            print('Ugyldig valg, prøv igjen.')


# Starter programmet
if __name__ == '__main__':
    main()

Merk at vi hele tiden bruker logikken vi har laget i metoder i klassen Huskeliste. Dette gir oss god kontroll på hva som skjer, samtidig som brukeren ikke trenger å bry seg med hvordan vi oppdaterer oppgaver eller lagrer filer.

Hvis du ønsker å teste din egen forståelse kan du før du går videre gjøre to forbedringer til main.py:

  • Det er ingenting som hindrer brukeren i å gi en oppgave negativ poengsum. Dette er uheldig å tillate. Bruk det du har lært i seksjon 12.2 og seksjon 12.3 til å frembringe og deretter håndtere en kjørefeil dersom brukeren skriver inn negative poengsummer.

  • Legg inn et alternativ til brukeren for å sortere huskelisten. Du kan for eksempel gi brukeren informasjon om at dersom hen skriver inn “SA” så vil huskelisten bli skrevet ut sortert alfabetisk, mens hvis hen skriver inn “SP” så vil huskelisten bli skrevet ut sortert etter poengsum. Husk at vi allerede har laget en metode .sorter_oppgaver() i klassen Huskeliste som sorterer huskelisten for oss, så du bør bruke denne.

Så langt får brukeren poeng for fullførte oppgaver, men vi har enda ikke et nivåsystem. Dette skal vi legge til i neste seksjon.

14.6 Spillifisering med nivåer

Nå som vi har det tekniske fundamentet på plass, er det på tide å legge til det som gjør dette til mer enn bare en kjedelig liste: selve spillet. I programmeringsverdenen kaller vi ofte dette for “gamification”. Dette betyr at vi låner mekanikker fra spill for å gjøre hverdagslige oppgaver mer artige.

Det viktigste elementet i et slikt system er et nivå-system. Vi ønsker at brukeren skal starte på nivå 1, og klatre oppover etter hvert som poengene ruller inn.

14.6.1 Utregning av nivå

Vi må først bestemme oss for en matematisk regel for hvordan poeng blir til nivåer. Typisk sett kreves det en større mengde poeng for å nå nye nivåer etter hvert som du stiger i nivå. På den måten er det lett å gå opp i nivå i begynnelsen, men etter hvert krever det mer arbeid. En formel vi kan bruke for dette er:

\[Nivå = \Big\lfloor \sqrt{\frac{\text{Total poengsum}}{100}} \Big\rfloor + 1\]

Her bruker vi (\(\lfloor \dots \rfloor\)) som matematisk betyr å runde ned til nærmeste heltall. I Python kan vi bruke funksjonen int() for å oppnå dette.

Brukeren vil starte på nivå 1 og vil deretter møte på følgende nivågrenser:

  • Brukeren går opp til nivå 2 når du har 100 poeng.

  • Brukeren går opp til nivå 3 når du har 400 poeng.

  • Brukeren går opp til nivå 4 når du har 900 poeng.

  • Brukeren går opp til nivå 5 når du har 1600 poeng.

Som du ser, må du jobbe betydelig hardere for å komme deg fra nivå 4 til 5 enn du måtte for å nå nivå 2! La oss oppdatere klassen Huskeliste i filen kjerne.py. Vi skal bruke dekoratøren @property som vi lærte om i seksjon 11.5 for å regne ut nivået automatisk basert på poengsummen:

from datetime import date
from math import sqrt


class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""
    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    @property
    def nivå(self):
        """Regner ut nåværende nivå basert på total poengsum."""
        return int(sqrt(self.total_poeng / 100)) + 1
      
    # -- De andre metodene er fjernet for leselighet --

Ved å bruke dekoratøren @property på denne måten gjør vi det mindre sannsynlig at brukeren kan sette nivået direkte. Et nivå skal jo oppnås, ikke bare direkte settes ved å endre en verdi.

14.6.2 Visuell progresjon i terminalen

La oss nå vise en fremdriftslinje når huskelisten blir skrevet ut til terminalen. Det ser mye kulere ut enn bare et tall. Vi oppdaterer dundermetoden __str__ i Huskeliste slik at den inkluderer statusen vår:

from datetime import date
from math import sqrt


class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""
    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def __str__(self):
        # 1. Grunnleggende utregninger for progresjon
        poeng_start_nivå = 100 * (self.nivå - 1)**2
        poeng_neste_nivå = 100 * self.nivå**2

        poeng = self.total_poeng - poeng_start_nivå
        poenggrense = poeng_neste_nivå - poeng_start_nivå

        # 2. Konstruksjon av fremdriftslinje
        størrelse = 20
        andel = poeng / poenggrense
        fullført_felt = int(andel * størrelse)
        bar = '█' * fullført_felt + '░' * (størrelse - fullført_felt)

        # 3. Formatering av statusoverskrift
        status = f'NIVÅ: {self.nivå} |{bar}| ({poeng}/{poenggrense} poeng)'
        ramme = '=' * len(status)
        overskrift = f'\n{ramme}\n{status}\n{ramme}\n'
        
        # 4. Samling av alle linjer
        if not self.oppgaver:
            return f'{overskrift}\nHuskelisten er tom. Legg til noe å gjøre!'

        linjer = [overskrift]
        for i, oppgave in enumerate(self.oppgaver):
            linjer.append(f'{i + 1}. {oppgave}')
            
        return '\n'.join(linjer)
      
    # -- De andre metodene er fjernet for leselighet --

Du kan teste ut programmet nå selv med å fullføre et par oppgaver og deretter skrive ut huskelisten. Programmet ser en del bedre ut med en solid fremdriftslinje som viser at du er på riktig vei mot et nytt nivå.

14.6.3 Tilbakemelding til brukeren

Når vi fullfører en oppgave, bør vi sjekke om brukeren har gått opp et nivå. Dette kalles en “Level Up!”, og er det mest tilfredsstillende øyeblikket i ethvert spill. Gå til filen main.py og oppdater logikken for fullføring av oppgaver. Her er et tips til hvordan du kan sjekke om nivået har endret seg:

from kjerne import Oppgave, Huskeliste
from tjenester import lagre_til_fil, last_fra_fil


def main():
    min_huskeliste = last_fra_fil()
    print('Velkommen til din spillifiserte huskeliste!')
    
    while True:
        print('\n--- MENY ---')
        print('1. Vis huskeliste')
        print('2. Legg til ny oppgave')
        print('3. Fullfør en oppgave')
        print('4. Slett en oppgave')
        print('5. Lagre og avslutt')
        
        valg = input('\nVelg et alternativ (1-5): ')

        if valg == '1':
            print('\n', min_huskeliste)
        elif valg == '2':
            tittel = input('Hva skal du gjøre? ')
            poeng = int(input('Hvor mange poeng er denne verdt? '))
            ny_oppgave = Oppgave(tittel, poeng)
            min_huskeliste.legg_til_oppgave(ny_oppgave)
        elif valg == '3':
            tittel = input('Hvilken oppgave har du fullført? ')
            oppgave = min_huskeliste.finn_oppgave(tittel)
            nivå_før = min_huskeliste.nivå
            if oppgave:
                min_huskeliste.fullfør_oppgave(oppgave)
                if min_huskeliste.nivå > nivå_før:
                    print(f'\n🌟 LEVEL UP! Nivå: {min_huskeliste.nivå}! 🌟')
            else:
                print(f'Fant ingen oppgave med navn {tittel}.')
        elif valg == '4':
            tittel = input('Hvilken oppgave vil du slette? ')
            oppgave = min_huskeliste.finn_oppgave(tittel)
            if oppgave:
                min_huskeliste.slett_oppgave(oppgave)
            else:
                print(f'Fant ingen oppgave med navn {tittel}.')
        elif valg == '5':
            lagre_til_fil(min_huskeliste)
            print('Programmet avsluttes. Lykke til videre!')
            break
        else:
            print('Ugyldig valg, prøv igjen.')


# Starter programmet
if __name__ == '__main__':
    main()

Dette lille grepet gjør at programmet føles levende. Brukeren får umiddelbar respons på innsatsen sin, noe som øker sjansen for at huskelisten faktisk blir brukt.

Her er de to andre filene kjerne.py og tjenester.py vi har skrevet slik at du dem lett tilgjengelig:

from datetime import date
from math import sqrt


class Oppgave:
    """Representerer en enkelt oppgave i huskelisten."""

    def __init__(self, tittel, poeng):
        self.tittel = tittel
        self.poeng = poeng
        self.er_fullført = False
        self.dato_opprettet = date.today().isoformat()
        self.dato_fullført = None

    def marker_fullført(self):
        """Markerer oppgaven som fullført og setter dato for fullføring."""
        if not self.er_fullført:
            self.er_fullført = True
            self.dato_fullført = date.today().isoformat()
            return True
        return False

    def __str__(self):
        status = '[X]' if self.er_fullført else '[ ]'
        return f'{status} {self.tittel} - {self.poeng} poeng'


class Huskeliste:
    """Representerer huskelisten som en samling med oppgaver."""

    def __init__(self):
        self.oppgaver = []
        self.total_poeng = 0

    def finn_oppgave(self, tittel):
        """Leter etter en oppgave med gitt tittel og returnerer objektet."""
        for oppgave in self.oppgaver:
            if oppgave.tittel.lower() == tittel.lower():
                return oppgave

    def legg_til_oppgave(self, ny_oppgave):
        """Legger til en ny oppgave til huskelisten."""
        if ny_oppgave.tittel not in [oppgave.tittel for oppgave in self.oppgaver]:
            self.oppgaver.append(ny_oppgave)
        else:
            print('Oppgaven eksisterer allerede i huskelisten.')

    def fullfør_oppgave(self, oppgave):
        """Finner en oppgave basert på tittel og tildeler poeng."""
        if oppgave in self.oppgaver and oppgave.marker_fullført():
            self.total_poeng += oppgave.poeng
            print(f'Fullført {oppgave.tittel} + {oppgave.poeng} poeng.')
            return True
        return False

    def slett_oppgave(self, oppgave):
        """Sletter en oppgave fra huskelisten."""
        if oppgave in self.oppgaver:
            self.oppgaver.remove(oppgave)
            return oppgave
        else:
            print(f'Oppgaven {oppgave.tittel} eksisterer ikke!')

    def sorter_oppgaver(self, kriterium='alfabetisk'):
        """Sorterer oppgavene basert på et kriterium."""
        if kriterium == 'alfabetisk':
            self.oppgaver.sort(
                key=lambda oppgave: oppgave.tittel
            )
        elif kriterium == 'poengsum':
            self.oppgaver.sort(
                key=lambda oppgave: oppgave.poeng,
                reverse=True
            )
        else:
            print('Dette er ikke et gyldig kriterium.')

    @property
    def nivå(self):
        """Regner ut nåværende nivå basert på total poengsum."""
        return int(sqrt(self.total_poeng / 100)) + 1

    def __str__(self):
        # 1. Grunnleggende utregninger for progresjon
        poeng_start_nivå = 100 * (self.nivå - 1)**2
        poeng_neste_nivå = 100 * self.nivå**2

        poeng = self.total_poeng - poeng_start_nivå
        poenggrense = poeng_neste_nivå - poeng_start_nivå

        # 2. Konstruksjon av fremdriftslinje
        størrelse = 20
        andel = poeng / poenggrense
        fullført_felt = int(andel * størrelse)
        bar = '█' * fullført_felt + '░' * (størrelse - fullført_felt)

        # 3. Formatering av statusoverskrift
        status = f'NIVÅ: {self.nivå} |{bar}| ({poeng}/{poenggrense} poeng)'
        ramme = '=' * len(status)
        overskrift = f'\n{ramme}\n{status}\n{ramme}\n'

        # 4. Samling av alle linjer
        if not self.oppgaver:
            return f'{overskrift}\nHuskelisten er tom. Legg til noe å gjøre!'

        linjer = [overskrift]
        for i, oppgave in enumerate(self.oppgaver):
            linjer.append(f'{i + 1}. {oppgave}')

        return '\n'.join(linjer)
import json
from kjerne import Oppgave, Huskeliste


def lagre_til_fil(huskeliste, filnavn='oppgaver.json'):
    """Konverterer huskelisten til en ordbok og lagrer den som JSON."""
    data = {
        'total_poeng': huskeliste.total_poeng,
        'oppgaver': [oppgave.__dict__ for oppgave in huskeliste.oppgaver]
    }

    with open(filnavn, 'w', encoding='utf-8') as fil:
        fil.write(json.dumps(data, ensure_ascii=False, indent=4))
    print(f'Data lagret til {filnavn}')


def last_fra_fil(filnavn='oppgaver.json'):
    """Leser en JSON-fil og gjenskaper Huskeliste- og Oppgave-objekter."""
    try:
        with open(filnavn, 'r', encoding='utf-8') as fil:
            data = json.load(fil)

        ny_huskeliste = Huskeliste()
        ny_huskeliste.total_poeng = data.get('total_poeng', 0)

        for informasjon in data.get('oppgaver', []):
            oppgave = Oppgave(informasjon['tittel'], informasjon['poeng'])
            oppgave.er_fullført = informasjon['er_fullført']
            oppgave.dato_opprettet = informasjon['dato_opprettet']
            oppgave.dato_fullført = informasjon['dato_fullført']
            ny_huskeliste.legg_til_oppgave(oppgave)

        return ny_huskeliste

    except FileNotFoundError:
        return Huskeliste()

14.7 Videre utvidelser av programmet

Du har nå laget et fullt fungerende system for å håndtere oppgaver, men i programmering blir man sjelden helt ferdig. Her er tre forslag til hvordan du kan ta prosjektet ditt et steg videre:

  • 1. Prioritering av oppgaver: For å gjøre listen mer oversiktlig, kan du legge til et prioriteringsnivå på hver oppgave (f.eks. Høy, Middels og Lav). Oppdater funksjonen for å legge til oppgaver slik at den spør etter prioritet. Når du viser listen, kan du sortere den slik at de viktigste oppgavene alltid vises øverst.

  • 2. Datavisualisering: Når du har brukt programmet over tid, vil du sitte på mye data om din egen produktivitet. Ved å bruke eksterne biblioteker som matplotlib kan du gjøre denne informasjonen om til bilder. Lag en funksjon som teller hvor mange oppgaver du fullfører hver måned, og presenter dette i et stolpediagram. Dette gir en visuell bekreftelse på hvilke perioder du har vært mest effektiv.

  • 3. Arkivering og automatisk rydding: Etter hvert som listen over fullførte oppgaver vokser, kan oppgaver.json-filen bli unødvendig stor og treg å lese. Lag en funksjon for “Arkivering”. Denne kan for eksempel flytte alle fullførte oppgaver som er eldre enn 30 dager over i en egen fil (arkiv.json). På denne måten holder du hovedprogrammet raskt og oversiktlig.

Jeg anbefaler at du velger ett av alternativene over og prøver å implementere dette selv. Du vil lære mye av å skrive ny funksjonalitet til programmet uten at jeg leder deg gjennom det.

14.8 Takk for reisen!

Du har nå gått fra å kanskje bare ha en vag idé om hva koding er, til å bygge et komplett, logisk og nyttig program fra grunnen av. Veien gjennom denne boken har ikke bare handlet om å lære seg Python-syntaks, men om å lære seg en ny måte å tenke på.

Programmering er et håndverk. Akkurat som en snekker aldri slutter å lære om nye materialer eller teknikker, vil det alltid finnes nye biblioteker, rammeverk og metoder å utforske i kodingens verden.

Takk for at du valgte å legge ut på denne reisen sammen med meg. Du har nå verktøyene som trengs for å skape nesten hva som helst. Det er ikke lenger koden som setter grenser, men fantasien din.

Lykke til med dine fremtidige prosjekter!