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.
Dette kapitlet trener Oppgave 4, og bare den.
Sjanger F — stor klassemodellering står i 6 av 6 moderne eksamenssett
(100 %) og er verdt 47–50 poeng av 100 — nesten halve eksamen. Den er delt i seks–sju deloppgaver à 3–12 poeng, og hver av dem rettes for seg.
Dette må sitte — bokas høyeste prioritetsnivå.
Domenet roterer fra år til år, men den underliggende øvelsen er identisk: les en tekst, finn klassene, bygg dem, fyll dem fra en fil, og legg én–to smarte metoder på toppen. Denne drillen roterer domenet på samme måte, med nyskrevne innpakninger.
To taktiske regler er verdt like mye som kodeferdighetene. Du skal aldri stå
fast — hopp til neste deloppgave, som rettes uansett. Og du kan bruke en metode oppgaveteksten beskriver selv om du ikke har rukket å skrive den.
Kapitlet er langt (~90 min) og er delt i fire økter med tidsanslag. Ta gjerne én økt om gangen.
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 x1202. 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
None3. 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:2fil = 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 3De 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.
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 medself._. 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.
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.
> 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 ogfinn. c) Skriv les_fra_fil. d) Skrivmest_populaere_forfatter. e) Skriv __str__ og skriv_alle.
| Klasse | Instansvariabler | Metoder |
|---|---|---|
Bok | _tittel, _forfatter, _utlaan | hent_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:2Hele 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
NoneSensors margnotater, deloppgave for deloppgave.
a) Klassen Bok (8 p): class-linjen (1 p), __init__ med treself._-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.
> 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.
> 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å.
> 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?
> 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.
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.
> 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.
Fem feil koster mest i Oppgave 4:
- #1 — self-fella, i konstruktøren eller i en metode. Verdien blir lokal og
forsvinner, og feilen er stum hvis det er satt en startverdi.
- #11 — obj._x utenfra. Den vanligste formen når to klasser samarbeider:
self._d[bok._tittel] = bok i stedet for bok.hent_tittel().
- Rådata i stedet for objektreferanser. Koster i fire deloppgaver samtidig, og
er den dyreste enkeltbeslutningen.
- #14 — oppslag uten sjekk, og #7 — return inne i løkka. To
plasseringsfeil som begge er lette å unngå med malene.
- Feil samlingstype. «Finne på navn» betyr ordbok; «gå gjennom alle» betyr
liste. Har klassen begge behov, kan den ha begge samlinger.
Og én navnefeil: containerens metode som heter det samme som elementets.
En metode som kaller r.selg(...), kan ikke selv hete selg. Bruk selg_rett og skriv_alle.
Begrepsbank
Begrepsbanken er flashcard- og repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.
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.
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.
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.
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.
Å lagre rådata i stedet for objektreferanser i samlingen.
self._boker[tittel] = felt[1] i stedet forself._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.
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.
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?».
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.
r = self.finn(navn) og deretter if r == None: før du bruker r.Uten sjekken stopper programmet medAttributeError: '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.
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.
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.
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.
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 medself._? Står det noe obj._x utenfor klassen sin? Har hvert oppslag enif-sjekk foran? Står return etter løkkene, ikke inne i dem?
De stumme feilene finnes ikke ved å teste — bare ved å lese.
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.
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.