Tilbake
6.5

6.5 DRILL — Full klassemodellering fra spesifikasjon

Den fullt gjennomarbeidede malen for Oppgave 4: fra spesifikasjon til 3–5 samarbeidende klasser med konstruktør, aksessor/mutator, «en-til-mange», filinnlesing, algoritme-metode, `__str__` og input — i sensors korte, eksplisitte stil, med sensor-kommentarer.

90 min
6 oppgaver
DRILLFull klassemodellering fra spesifikasjon
Din fremgang i kapitlet
0 / 6 oppgaver

Forkunnskaper — sist du var her

Kapitlet samler hele del 5 og 6, og låner én konstruksjon fra del 7:
kap. 5.1 klasser,
kap. 5.2 innkapsling,
kap. 5.3 __str__ og delegering,
kap. 6.1 modellering,
kap. 6.2 en-til-mange,
kap. 6.3 algoritme-metoder,
kap. 6.4 terminal-interaksjon og
kap. 7.1 filinnlesing.

De tre bitene som gjentar seg i hver modell, friskes opp med kjørt kode.

1. Klasse med konstruktør, aksessor og __str__:

class Vare:
    def __init__(self, navn, antall):
        self._navn = navn self._antall = antall

    def hent_navn(self):
        return self._navn

    def hent_antall(self):
        return self._antall

    def __str__(self):
        return self._navn + " x" + str(self._antall)

print(Vare("skrue", 120))

Utskrift:

skrue x120

2. Container med ordbok, oppslag og delegering:

class Vare:
    def __init__(self, navn):
        self._navn = navn

    def hent_navn(self):
        return self._navn

    def __str__(self):
        return self._navn

class Lager:
    def __init__(self):
        self._varer = {}

    def legg_til(self, v):
        self._varer[v.hent_navn()] = v

    def finn(self, navn):
        if navn in self._varer:
            return self._varer[navn]
        return None

    def skriv_alle(self):
        for n in self._varer:
            print(self._varer[n])

l = Lager()
l.legg_til(Vare("skrue"))
l.skriv_alle()
print(l.finn("bolt"))

Utskrift:

skrue
None

3. Filinnlesing-løkka fra kap. 7.1.
Merk at det kapitlet står lenger bak i boka. Leser du rett fram, møter du
løkka her først — derfor står hele mønsteret oppfrisket her, kjørt og med utskrift, slik at du kan lese kapitlet uten å hoppe fram.

Innholdet i utlaan.txt:

Sult:Hamsun:3
Vaar:Undset:7
Naboer:Bjornson:2

fil = open("utlaan.txt")
linje = fil.readline()
while linje != "":
    felt = linje.rstrip().split(":")
    print(felt[0], int(felt[2]) + 1)
    linje = fil.readline()
fil.close()

Utskrift:

Sult 4
Vaar 8
Naboer 3

De fem delene er open før løkka, readline før løkka,
while linje != "":, kroppen med rstrip() og split(...), og readline nederst — og fil.close() etter.

Har du de tre, er resten rekkefølge og disiplin.

📜Løsningsoppskriften for Oppgave 4 — ti steg

Bruk den samme framgangsmåten hver gang, uansett domene. De fire første er blyant og papir; resten er kode.

Steg 1 — Finn klassene. Hvert substantiv som er en ting med egne
egenskaper
, blir en klasse. Typisk tre til fem.

Steg 2 — Sett opp konstruktørene. Hver egenskap blir en instansvariabel med
self._. Husk startverdiene som ikke kommer utenfra: 0, False, [], {}.

Steg 3 — Finn multiplisiteten. «Én» gir self._eier = objekt; «flere» gir en
liste eller en ordbok. Ordbok når noe skal slås opp på navn.

Steg 4 — List opp metodene. Hver «kan gjøre»-setning blir én metode, plassert
i den klassen som eier opplysningene den endrer.

Steg 5 — Skriv aksessorer og mutatorer for det oppgaven faktisk trenger — og
bare det.

Steg 6 — Skriv fil-deloppgaven. Samlingen før løkka, .rstrip().split(sep),
int(...) på tallfeltene, objektet opprettet og lagt i samlingen.

Steg 7 — Skriv delegeringsmetodene. Løkka kaller én metode på
underobjektet. Containerens metode heter noe annet enn elementets.

Steg 8 — Skriv algoritme-metoden. Finn beste, tell eller grupper. To variabler
før løkka, return etter.

Steg 9 — Skriv __str__ og eventuell input-metode.

Steg 10 — Dokumentér antakelser i én kommentarlinje der teksten er
underspesifisert, og skriv en kommentar om at hver klasse hører hjemme i sin egen fil.

Tidsbudsjettet: Oppgave 4 er verdt rundt 100 minutter av de fire timene.
Bruk de første ti på steg 1–4. Det er den beste investeringen i hele eksamen.

Rekkefølgen deloppgavene kommer i

Settene følger nesten samme rekkefølge hvert år:

a) klassene med konstruktør · b) «en-til-mange» og innsetting ·
c) filinnlesing · d) algoritme-metode · e) __str__ eller
utskriftsmetode · f) input-metode · g) av og til et teorispørsmål om grensesnittet.

Rekkefølgen er ikke tilfeldig: hver deloppgave bygger på den forrige. Men de rettes hver for seg, så du kan hoppe over c) og fortsatt få full uttelling på d).

Og du kan bruke en metode oppgaven beskriver uten å ha skrevet den. Sier teksten at Bok har hent_tittel(), kall den i d) selv om du sto fast i a).

Økt 1 — Den gjennomarbeidede casen (~25 min)

Under står en komplett Oppgave 4 med alle deloppgavene, og med margnotater om hva som gir uttelling ved hvert steg. Les den som om det var eksamensdagen.

✏️Eksempel 1: Full Oppgave 4 — bibliotek, med sensor-margnotater

> Et bibliotek har et navn og en samling bøker. Hver bok har en tittel, en
> forfatter og et antall utlån. En bok kan lånes ut, som øker antall utlån med én.
> Biblioteket kan legge til en bok, finne en bok på tittel, lese inn bøker fra en
> fil, skrive ut hele samlingen, og si hvilken forfatter som har flest utlån til
> sammen.
>
> Fila utlaan.txt har linjer på formen tittel:forfatter:utlaan.

a) Skriv klassen Bok. b) Skriv Bibliotek med samlingen, legg_til og
finn. c) Skriv les_fra_fil. d) Skriv
mest_populaere_forfatter. e) Skriv __str__ og skriv_alle.

Steg 1–4 på papir, før én linje kode:

KlasseInstansvariablerMetoder
Bok_tittel, _forfatter, _utlaanhent_tittel, hent_forfatter, hent_utlaan, laan_ut, __str__
Bibliotek_navn, _boker (ordbok)legg_til, finn, les_fra_fil, skriv_alle, mest_populaere_forfatter

Ordbok fordi teksten sier «finne en bok på tittel».
Innholdet i utlaan.txt:
Sult:Hamsun:3
Vaar:Undset:7
Naboer:Bjornson:2

Hele modellen:

class Bok:
    def __init__(self, tittel, forfatter, utlaan):
        self._tittel = tittel self._forfatter = forfatter self._utlaan = utlaan

    def hent_tittel(self):
        return self._tittel

    def hent_forfatter(self):
        return self._forfatter

    def hent_utlaan(self):
        return self._utlaan

    def laan_ut(self):
        self._utlaan = self._utlaan + 1

    def __str__(self):
        return self._tittel + " av " + self._forfatter + " (" + str(self._utlaan) + ")"

class Bibliotek:
    def __init__(self, navn):
        self._navn = navn self._boker = {}

    def legg_til(self, bok):
        self._boker[bok.hent_tittel()] = bok

    def finn(self, tittel):
        if tittel in self._boker:
            return self._boker[tittel]
        return None

    def les_fra_fil(self, filnavn):
        fil = open(filnavn)
        linje = fil.readline()
        while linje != "":
            felt = linje.rstrip().split(":")
            self.legg_til(Bok(felt[0], felt[1], int(felt[2])))
            linje = fil.readline()
        fil.close()

    def skriv_alle(self):
        print("Samling:", self._navn)
        for tittel in self._boker:
            print(" -", self._boker[tittel])

    def mest_populaere_forfatter(self):
        teller = {}
        for tittel in self._boker:
            b = self._boker[tittel]
            f = b.hent_forfatter()
            if f not in teller:
                teller[f] = 0 teller[f] = teller[f] + b.hent_utlaan()
        beste = None beste_antall = 0 for f in teller:
            if teller[f] > beste_antall:
                beste = f beste_antall = teller[f]
        return beste

bib = Bibliotek("Deichman")
bib.les_fra_fil("utlaan.txt")
bib.finn("Sult").laan_ut()
bib.finn("Sult").laan_ut()
bib.skriv_alle()
print("Mest populaer:", bib.mest_populaere_forfatter())
print(bib.finn("Pan"))
Utskrift:

Samling: Deichman
 - Sult av Hamsun (5)
 - Vaar av Undset (7)
 - Naboer av Bjornson (2)
Mest populaer: Undset
None

Sensors margnotater, deloppgave for deloppgave.

a) Klassen Bok (8 p): class-linjen (1 p), __init__ med tre
self._-linjer (3 p), de tre aksessorene (2 p), laan_ut som øker i stedet for å overskrive (2 p).

b) Bibliotek med ordbok (8 p): self._boker = {} i konstruktøren (2 p),

innsettingen med bokas egen aksessor som nøkkel (3 p), if tittel in ...-sjekken og return None (3 p). self._boker[bok._tittel] ville vært feilkode #11.
c) les_fra_fil (10 p): open (1 p), readline før løkka (1 p),

while linje != "": (1 p), .rstrip().split(":") (2 p), int(felt[2]) men ikke på tittel og forfatter (2 p), objektet opprettet og lagt inn (2 p), readline nederst (1 p). At metoden kaller self.legg_til(...) i stedet for å skrive rett i ordboka, er ryddig, men gir ikke ekstra poeng.
d) mest_populaere_forfatter (12 p, den tyngste): teller-ordboka med

if f not in teller: (4 p), akkumuleringen med bokas utlån (2 p), finn-beste-løkka med to variabler oppdatert sammen (4 p), return etter løkka (2 p).
e) __str__ og skriv_alle (8 p): __str__ returnerer en tekst med

str(...) rundt tallet (4 p), skriv_alle delegerer med print(...) i stedet for å hente feltene (4 p).
Legg merke til at «Sult» har fem utlån. Fila sa 3, og bib.finn("Sult") ga en

referanse til det samme objektet begge gangene — ikke en kopi. Det er
referansesemantikken fra kap. 5.4 i praksis.

Riktig struktur belønnes selv med småfeil. En glemt .rstrip() koster lite når
resten står. Å lagre rådata i ordboka i stedet for objekter, koster derimot i
fire deloppgaver samtidig — det er den dyreste enkeltbeslutningen.

— naturlig pausepunkt —

Økt 2 — To modelleringer fra bunnen (~25 min)

De to første oppgavene er komplette Oppgave 4-sett i mindre format. Bruk de ti stegene, og bruk de første minuttene på blyant.

📝Oppgave 1
Eksamensnivå, sjanger F

> En resirkuleringsstasjon har et sted og flere containere. Hver container har et
> materiale, en kapasitet i kilo og en fylling som starter på null. En container
> kan ta imot avfall, men bare hvis det er plass til alt. Stasjonen kan sette inn
> en container, finne en container på materiale, skrive ut alle, og si hvilket
> materiale som har høyest fyllingsgrad i prosent.

a) Skriv Container med konstruktør, aksessorer, fyllingsgrad(self) i
prosent, ta_imot(self, kilo) og __str__ på formen glass: 60/200 kg.

b) Skriv Stasjon med samlingen, sett_inn, finn og skriv_alle.

c) Skriv fulleste(self) som returnerer containeren med høyest
fyllingsgrad.

d) Test hele modellen, med ett avvist forsøk.

📝Oppgave 2
Eksamensnivå, sjanger F

> Et treningssenter har et navn, medlemmer og økter. Hvert medlem har et navn, en
> alder og et antall timer. Hver økt har en dag, et salnavn og et antall
> deltakere. Senteret kan lese medlemmer fra en fil, finne et medlem på navn,
> legge til en økt, skrive ut alle medlemmene, og si hvilken dag som er travlest.
>
> Fila medlemsregister.txt har en overskriftslinje og deretter linjer på
> formen navn;alder;timer.

a) Skriv Medlem og Okt.

b) Skriv Senter med en ordbok for medlemmene og en liste for øktene.

c) Skriv les_medlemmer(self, filnavn), som hopper over overskriftslinja.

d) Skriv travleste_dag(self).

e) Test hele modellen.

Økt 3 — Modelleringer med input og tre klasser (~22 min)

De to neste oppgavene legger til de siste bitene: en input-metode, og en tredje klasse som objektene peker på.

📝Oppgave 3
Eksamensnivå, sjanger F…

> En brettspillklubb har et navn, medlemmer og spill. Hvert medlem har et navn og
> et antall kvelder som starter på null. Hvert spill har en tittel, et antall
> spillere det passer for, og én eier, som er et medlem. Klubben kan melde inn
> et medlem, legge til et spill, finne et spill på tittel, skrive ut alle spillene,
> og la brukeren velge et spill fra en liste — der spørsmålet gjentas til tittelen
> finnes.

a) Skriv de tre klassene.

b) Skriv velg_spill(self) i eksamensform, med input.

c) Skriv en kjørbar variant av hele modellen, der «brukerens» svar er
"Yatzy" og "Sjakk".

d) Hvorfor returnerer velg_spill objektet og ikke tittelen?

📝Oppgave 4
Eksamensnivå, sjanger F

> En sykkelutleie har et navn og flere stativer. Hvert stativ har et navn, et
> antall plasser og en samling sykler som står der nå. Hver sykkel har en kode og
> et antall turer som starter på null. En sykkel kan leies ut, som øker antall
> turer. Et stativ kan sette inn en sykkel, men bare hvis det er ledig plass, og
> si hvor mange plasser som er ledige. Utleien kan legge til et stativ, finne et
> stativ på navn, skrive ut alle stativene, og si hvor mange sykler det er til
> sammen.

a) Skriv de tre klassene.

b) Test med to stativer, fire sykler og ett avvist forsøk.

c) Hvorfor heter metoden i Utleie som teller sykler, ikke det samme som
metoden i Stativ?

Økt 4 — Feilsøking og de siste to modellene (~18 min)

De siste oppgavene trener det å finne feil i en modell — som er halve ferdigheten når du leser gjennom din egen besvarelse til slutt.

📝Oppgave 5
Eksamensnivå, sjanger I

En medstudent har levert denne besvarelsen på en Oppgave 4:

class Rett:
    def __init__(self, navn, pris):
        self._navn = navn pris = pris self._solgte = 0

    def selg(self, antall):
        solgte = self._solgte + antall

    def omsetning(self):
        return self._pris * self._solgte

class Kantine:
    def __init__(self):
        self._retter = {}

    def legg_til(self, r):
        self._retter[r._navn] = r

    def finn(self, navn):
        return self._retter[navn]

a) Finn de fire feilene.

b) Gi et konkret moteksempel på den som stopper programmet først.

c) Skriv den rettede versjonen.

📝Oppgave 6
Eksamensnivå, sjanger F

> En kantine har et navn og en meny. Hver rett har et navn, en pris og et antall
> solgte porsjoner som starter på null. En rett kan selges, og kan si hva den har
> omsatt for. Kantina kan lese menyen fra en fil, finne en rett på navn, selge en
> rett, skrive ut hele menyen, si den samlede omsetningen, og si hvilken rett som
> er bestselger.
>
> Fila retter.txt har en overskriftslinje og deretter linjer på formen
> rett,pris,solgte.

Skriv hele modellen og test den. Marker selv hvilke deloppgaver du løser.

API- og konstruksjonsliste

Begrepsbank

Begrepsbanken er flashcard- og repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.

Oppgave 4 — formen

Seks–sju deloppgaver à 3–12 poeng, til sammen 47–50 av 100. Sjanger F, 100 % frekvens i de seks moderne settene.

Rekkefølgen er nesten fast: klasser, en-til-mange, filinnlesing, algoritme-metode,
__str__, input, og av og til et teorispørsmål om grensesnittet.

Domenet roterer, men øvelsen er identisk hvert år.

De ti stegene

Fire på papir, seks i kode: finn klassene, sett opp konstruktørene, avgjør multiplisiteten, list opp metodene — så aksessorer, fil, delegering, algoritme,
__str__, og dokumenterte antakelser.

De fire første tar ti minutter og avgjør resten. Det er den beste investeringen i hele eksamen.

Motsatt rekkefølge — å skrive ferdig kode på den første klassen med én gang — er den vanligste tidsfellen.

Delkarakter per deloppgave

Hver deloppgave rettes for seg. Bommer du på c), teller d) fullt ut.

Konsekvensen er taktisk: stå aldri fast. Skriv det du er sikker på, og gå videre.

Og du kan bruke en metode oppgaveteksten beskriver uten å ha skrevet den.
Sier teksten at Bok har hent_tittel(), kall den — sensor vet at den finnes, fordi oppgaven sa det.

Riktig struktur belønnes

En indeksfeil, en glemt aksessor eller en litt klønete betingelse trekker lite når modellen er riktig.

Det motsatte gjelder også: perfekt skrevne metoder på en gal modell hjelper ikke, fordi feilen gjentar seg i hver deloppgave.

Derfor er de første ti minuttene med blyant verdt mer enn de siste ti med tastatur.

Den dyreste enkeltbeslutningen

Å lagre rådata i stedet for objektreferanser i samlingen.

self._boker[tittel] = felt[1] i stedet for
self._boker[tittel] = Bok(...) gjør at biblioteket ikke lenger kan be en bok om noe som helst.

Da faller filinnlesingen, delegeringen, algoritme-metoden og utskriften — fire deloppgaver på én linje.

To samlinger i samme klasse

En klasse kan godt ha både en ordbok og en liste: en ordbok for det som skal slås opp på navn, og en liste for det som bare skal gås gjennom.

self._medlemmer = {} og self._okter = [] i samme konstruktør er helt vanlig i
Oppgave 4.

Å velge riktig type for hver av dem er et selvstendig modelleringspoeng.

To nivåer av en-til-mange

En utleie har mange stativer, og hvert stativ har mange sykler.

Hvert nivå følger nøyaktig samme mønster: samling i konstruktøren, legg_til, løkke som delegerer.

Blander du nivåene — for eksempel ved å la utleien holde alle syklene direkte — mister du muligheten til å svare på «hvor mange står på Blindern?».

Metoden som gjenbruker en annen
def selg_rett(self, navn, antall): som først kaller self.finn(navn) i stedet for å slå opp på nytt.

Gevinsten er at oppslagslogikken — inkludert if-sjekken — bare finnes ett sted.
Endrer du den, treffer endringen begge metodene.

Husk at metoden som kaller, må hete noe annet enn den som kalles, når begge ligger på ulike klasser i samme kjede.

Håndtering av `None` fra et søk
r = self.finn(navn) og deretter if r == None: før du bruker r.

Uten sjekken stopper programmet med
AttributeError: 'NoneType' object has no attribute '…' så snart noen slår opp noe som ikke finnes.

Å skrive en beskjed i None-grenen er som regel nok. Oppgaven sier sjelden hva som skal skje — dokumentér valget i en kommentar.

Filinnlesing som metode
def les_fra_fil(self, filnavn): fyller objektets egen samling og returnerer ingenting.

Skriver du retter = {} uten self. inne i metoden, blir ordboka lokal og forsvinner — feilkode #1 i en metode, og en stum feil.

Metoden kan gjerne kalle self.legg_til(...) i stedet for å skrive rett i samlingen. Det er ryddig, men det gir ikke ekstra poeng.

Å bygge regnskapet i én løkke og lese det i neste

Formen den tyngste deloppgaven ofte har: teller-ordbok først, finn-beste etterpå.

Summen for en nøkkel finnes ikke i noe enkelt element, så regnskapet må være ferdig før det kan leses.

Legg gjerne den første løkka i sin egen metode og kall den med self. foran. Da kan begge delene brukes hver for seg, og deloppgavene rettes uavhengig.

Navneregelen i en full modell

Containerens metode heter aldri det samme som elementets — heller ikke når klassene ligger på ulike nivåer.

En metode på kantina som heter selg og kaller selg på retten, ser ut som en metode som kaller seg selv. En metode som kaller seg selv, er rekursjon, som er utenfor pensum i dette emnet.

Bruk selg_rett, skriv_alle, totalt_antall. Det koster ingenting og gjør besvarelsen lettere å lese.

Gjennomlesingen til slutt

Sett av fem minutter til å lese gjennom besvarelsen med konvensjonene i hodet.

Fire spørsmål fanger det meste: starter hver tilordning i konstruktørene med
self._? Står det noe obj._x utenfor klassen sin? Har hvert oppslag en
if-sjekk foran? Står return etter løkkene, ikke inne i dem?

De stumme feilene finnes ikke ved å teste — bare ved å lese.

Tidsbudsjettet for Oppgave 4

Rundt 100 minutter av de fire timene, altså nesten halve eksamen — som seg hør og bør, siden den er nesten halve poengsummen.

Bruk de første ti på steg 1–4 med blyant. Bruk de siste fem på gjennomlesingen.
Da har du 85 minutter til koden, fordelt på seks–sju deloppgaver.

Blir du sittende fast på én i mer enn ti minutter, gå videre. Den rettes uansett ikke bedre av at du bruker tjue.

Repetisjon — de åtte setningene som må sitte

Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.

Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.