6.2 Flere samarbeidende klasser og «en-til-mange»
Modellere «en-til-mange» med en **liste eller ordbok av objektreferanser**, og la metoder **delegere** til underobjektenes metoder — kjernen i den store oppgaven.
en klasse som holder mange av en annen — og minst én metode som delegerer, altså løper gjennom samlingen og ber hvert underobjekt gjøre jobben.
Sjangeren er F — stor klassemodellering, altså å bygge flere samarbeidende klasser fra en beskrevet oppgave. Oppgave 4 er 47–50 poeng av 100, og dette kapitlet er selve motoren i den.
Dette må sitte — bokas høyeste prioritetsnivå.
Mønsteret er kort: en samling i konstruktøren, en legg_til-metode, et oppslag med en sjekk foran, og en løkke som delegerer. Fire biter, og de kommer igjen hvert år.
Kapitlet er langt (~60 min) og er delt i fire løkker med tidsanslag.
Forkunnskaper — sist du var her
Kapitlet bygger på kap. 6.1 — Modelleringsmetoden og
kap. 5.4 — Referansesemantikk, og bruker
kap. 3.2 — Ordbøker og
kap. 5.3 — __str__.
De tre tingene kapitlet hviler på, friskes opp med kjørt kode.
1. En samling kan holde objekter, ikke bare tall og tekster:
class Bok:
def __init__(self, tittel):
self._tittel = tittel
def __str__(self):
return self._tittel
boker = []
boker.append(Bok("Sult"))
boker.append(Bok("Vaar"))
print(len(boker), boker[0])Utskrift:
2 Sult2. En ordbok slås opp med if foran, ellers stopper programmet:
d = {"sult": 1}
if "sult" in d:
print(d["sult"])
if "vaar" in d:
print(d["vaar"])
else:
print("finnes ikke")Utskrift:
1 finnes ikke3. print(obj) kaller objektets egen __str__ — det er delegering i sin
enkleste form, fra kap. 5.3.
Nå setter vi de tre sammen inne i en klasse.
Løkke 1 — Samlingen i konstruktøren (~15 min)
Et bibliotek har mange bøker. Et treningssenter har mange saler. En klubb har mange medlemmer. Formen er den samme hver gang, og den heter en-til-mange.
I koden er den én linje i konstruktøren og én metode som legger til. Alt annet i kapitlet bygger på de to.
Bokas sekstende kodemønster. Tre linjer, og de er hele en-til-mange:
class Container:
def __init__(self, navn):
self._navn = navn self._elementer = []
def legg_til(self, e):
self._elementer.append(e)Blokken er en mal med plassholdernavn og er merket som ikke-kjørbar.
Fire regler følger med:
1. Samlingen opprettes i konstruktøren, tom. Ikke i legg_til, og ikke
utenfor klassen.
2. Samlingen holder objektreferanser, ikke rådata. append(bok), ikke
append(bok.hent_tittel()).
3. legg_til tar objektet som parameter. Den lager det ikke selv — det gjør
den som kaller.
4. Ordbok-varianten bytter self._elementer = {} og
self._elementer[e.hent_navn()] = e. Nøkkelen hentes med elementets egen
aksessor.
Bytter du liste mot ordbok, endres bare de to linjene. Resten av klassen står.
self._elementer = [] i konstruktøren, self._elementer.append(obj) ilegg_til.Bruk lista når du bare skal gå gjennom alle: skrive dem ut, telle dem, summere noe de svarer.
Rekkefølgen er den de ble lagt inn i, og du når dem både med for e in self._elementer: og med indeks self._elementer[0].
Skal du lete etter et bestemt element, må du løpe gjennom lista og sammenligne — det er fullt mulig og gir uttelling, men da er en ordbok som regel ryddigere.
self._elementer = {} i konstruktøren, og innsettingen bruker elementets egenaksessor som nøkkel:
class Sal:
def __init__(self, navn):
self._navn = navn
def hent_navn(self):
return self._navn
def __str__(self):
return "Sal " + self._navn
class Senter:
def __init__(self):
self._saler = {}
def legg_til(self, sal):
self._saler[sal.hent_navn()] = sal
s = Senter()
s.legg_til(Sal("Speilsal"))
s.legg_til(Sal("Spinning"))
print(len(s._saler))Utskrift:
2Utskriften bruker s._saler direkte, som boka ellers aldri gjør — feilkode
#11. Her er det gjort én gang for å vise at ordboka faktisk er fylt, siden
klassen ennå ikke har noen aksessor.
self._saler[sal.hent_navn()] = sal er den formen fasiten bruker: senteret
spør salen hva den heter, i stedet for å bestemme det selv.
Vis den samme modellen i to versjoner — én med liste og én med ordbok — og si når hver av dem er riktig.
Klassen Bok er den samme i begge. Forskjellen ligger i Bibliotek:
class Bok:
def __init__(self, tittel, forfatter):
self._tittel = tittel self._forfatter = forfatter
def hent_tittel(self):
return self._tittel
def __str__(self):
return self._tittel + " av " + self._forfatter
class BibliotekListe:
def __init__(self):
self._boker = []
def legg_til(self, bok):
self._boker.append(bok)
def antall(self):
return len(self._boker)
def skriv_alle(self):
for b in self._boker:
print(b)
class BibliotekOrdbok:
def __init__(self):
self._boker = {}
def legg_til(self, bok):
self._boker[bok.hent_tittel()] = bok
def antall(self):
return len(self._boker)
def finn(self, tittel):
if tittel in self._boker:
return self._boker[tittel]
return None
a = BibliotekListe()
a.legg_til(Bok("Sult", "Hamsun"))
a.legg_til(Bok("Vaar", "Undset"))
a.skriv_alle()
print("Liste:", a.antall())
b = BibliotekOrdbok()
b.legg_til(Bok("Sult", "Hamsun"))
b.legg_til(Bok("Vaar", "Undset"))
print("Ordbok:", b.antall(), b.finn("Vaar"), b.finn("Naboer"))Utskrift:
Sult av Hamsun
Vaar av Undset
Liste: 2
Ordbok: 2 Vaar av Undset NoneDe to klassene er nesten identiske. Bare to linjer skiller dem: [] mot {}
i konstruktøren, og append mot innsetting på nøkkel.
Det ordboka gir deg, er finn. Med lista måtte du løpt gjennom alle bøkene og
sammenlignet titlene — fullt mulig, men mer kode, og det er nettopp den koden oppgaveteksten sparer deg for når den sier «finne en bok på tittel».
Det lista gir deg, er rekkefølgen og enkelheten. Skal du bare skrive ut alle
eller telle noe, er den lettere å skrive riktig under tidspress.
Slik ville sensor sett på det: begge valg gir full uttelling så lenge de
metodene oppgaven ber om, virker. Valget teller først når teksten uttrykkelig ber om oppslag på en nøkkel — da er ordboka det som er «riktig modellert».
Merk at klassenavnene her har suffiks bare fordi begge versjonene skal kunne
kjøres i samme blokk. I en besvarelse ville klassen bare hett Bibliotek.
(Innstegsoppgave — gjengivelse.) Se på klassen:
class Klubb:
def __init__(self, navn):
self._navn = navn self._medlemmer = []
def meld_inn(self, m):
self._medlemmer.append(m)
def antall(self):
return len(self._medlemmer)a) Hvor opprettes samlingen, og hvorfor akkurat der?
b) Hva ville skjedd om self._medlemmer = [] hadde stått inne i meld_inn
i stedet?
c) Hva må meld_inn få inn — et navn eller et objekt?
Løkke 2 — Å slå opp uten å krasje (~14 min)
Ordboka er valgt fordi noe skal slås opp. Da kommer det neste spørsmålet med én gang: hva skjer når nøkkelen ikke finnes?
Svaret må stå i koden, ikke i hodet ditt. Uten en sjekk stopper programmet, og det er en av de vanligste feilene i den store oppgaven.
Standardformen på en finn-metode, i tre linjer:
class Register:
def __init__(self):
self._d = {}
def sett_inn(self, navn, verdi):
self._d[navn] = verdi
def finn(self, navn):
if navn in self._d:
return self._d[navn]
return None
r = Register()
r.sett_inn("ada", 24)
print(r.finn("ada"))
print(r.finn("nora"))Utskrift:
24
Noneif navn in self._d: sjekker at nøkkelen finnes. return self._d[navn]
gir objektet. return None er det ærlige svaret når det ikke finnes noe.
Den som kaller, tester med if svar != None:. Det er tryggere enn å returnere en tom tekst eller -1, som begge kan forveksles med ekte data — feilkode #10, sentinel-kollisjon.
Feilkode #14 er å slå opp en nøkkel som ikke er lagt inn. Programmet stopper:
d = {"sult": 1}
print(d["vaar"])Siste linje i feilmeldingen er KeyError: 'vaar' — med selve nøkkelen i.
Blokken er merket som ikke-kjørbar fordi feilen er plantet med vilje.
Den samme feilen i klasseform er en finn-metode uten if-sjekk. Den virker
perfekt i testen din, fordi du naturlig nok slår opp noe du nettopp la inn — og stopper første gang en bruker skriver noe annet.
Feilen har en tvillingbror: å legge til i en samling som ikke finnes ennå.
d = {}
d["ada"].append("Kolonier")Her stopper programmet på KeyError: 'ada', fordi det ikke finnes noen liste å legge i. Løsningen er den samme sjekken: if navn not in d: og d[navn] = [] først. Det er grupperings-idiomet fra
kap. 3.2, og det er feilkode #14 i sin andre drakt.
Vis hva som skjer med og uten if-sjekken i en finn-metode, og hvordan den som kaller, bruker svaret.
Klassen med sjekken på plass, og en kaller som håndterer begge utfall:
class Sal:
def __init__(self, navn, kapasitet):
self._navn = navn self._kapasitet = kapasitet
def hent_navn(self):
return self._navn
def hent_kapasitet(self):
return self._kapasitet
def __str__(self):
return self._navn + " (" + str(self._kapasitet) + ")"
class Senter:
def __init__(self):
self._saler = {}
def legg_til(self, sal):
self._saler[sal.hent_navn()] = sal
def finn(self, navn):
if navn in self._saler:
return self._saler[navn]
return None
s = Senter()
s.legg_til(Sal("Speilsal", 20))
s.legg_til(Sal("Spinning", 14))
for navn in ["Speilsal", "Basseng"]:
sal = s.finn(navn)
if sal != None:
print("Fant", sal, "med", sal.hent_kapasitet(), "plasser")
else:
print("Fant ingen sal som heter", navn)Utskrift:
Fant Speilsal (20) med 20 plasser
Fant ingen sal som heter BassengLegg merke til if sal != None: hos den som kaller. Uten den villesal.hent_kapasitet() stoppet programmet på det andre navnet, og siste linje i feilmeldingen ville værtAttributeError: 'NoneType' object has no attribute 'hent_kapasitet'.
Den feilmeldingen er verdt å kjenne igjen: «NoneType has no attribute»
betyr nesten alltid at et søk ga None, og at du glemte å sjekke.
Slik ville sensor sett på det: if navn in ...-sjekken og `return
None`-grenen er to selvstendige poeng, og de gis begge selv om resten av metoden skulle ha en feil. Å håndtere svaret riktig hos den som kaller, er et tredje.
Merk løkka for navn in ["Speilsal", "Basseng"]:. Den løper over en liste
skrevet rett inn i koden — en kjapp måte å teste begge utfall på uten å gjenta koden.
En sykkelutleie skal holde stativene sine i en ordbok fra stativnavn til Stativ-objekt.
a) Skriv klassen Stativ med navn, antall plasser, og en liste over syklene
som står der. Den skal ha hent_navn(), sett_inn(self, sykkel) ogantall_sykler(self).
b) Skriv klassen Utleie med ordboka, legg_til(self, stativ) ogfinn(self, navn).
c) Test: to stativer, tre sykler fordelt på dem, ett oppslag som lykkes og
ett som ikke gjør det.
Hva skriver programmet ut?
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
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 antall(self):
return len(self._varer)
lager = Lager()
lager.legg_til(Vare("skrue", 120))
lager.legg_til(Vare("bolt", 45))
lager.legg_til(Vare("skrue", 200))
print(lager.antall())
print(lager.finn("skrue").hent_antall())
print(lager.finn("mutter"))Løkke 3 — Delegering (~16 min)
Nå kommer mønsteret som er fast delkrav i hver Oppgave 4.
Ytterklassen skal skrive ut alle underobjektene, telle noe de vet, eller be dem gjøre noe. Den skal ikke grave i verdiene deres — den skal spørre.
Bokas syttende kodemønster. Én løkke, ett kall:
def skriv_alle(self):
for e in self._elementer:
print(e)
def totalt_antall(self):
total = 0 for e in self._elementer:
total = total + e.antall()
return totalBlokken er en mal med plassholdernavn og er merket som ikke-kjørbar.
Over en ordbok går løkka over nøklene, og elementet hentes med et oppslag:
def skriv_alle(self):
for n in self._elementer:
print(self._elementer[n])Tre regler:
1. Kroppen er ett kall på underobjektet, ikke et regnestykke over feltene
dets.
2. Aldri e._x. Det er feilkode #11, og det er den vanligste formen den
tar når to klasser samarbeider.
3. Akkumulatoren står før løkka og returneres etter, som ellers.
Gevinsten er den samme som i kap. 5.2: endrer underklassen hvordan den regner eller viser seg, merker ytterklassen ingenting.
En metode i denne boka kaller aldri en metode med samme navn — verken på seg selv eller på et underobjekt.
Grunnen er dels lesbarhet: de to gjør ulike ting. bok.skriv() viser én bok;bibliotek.skriv_alle() viser alle. To jobber, to navn.
Og dels er den praktisk. En linje som for b in self._boker: b.skriv() inne i en metode som selv heter skriv, ser ut som om metoden kaller seg selv. En metode som kaller seg selv, er rekursjon, som er utenfor pensum i dette emnet — så navnet ville sendt et feil signal til alle som leser koden.
Bruk disse navneparene:
| Elementets metode | Containerens metode |
|---|---|
skriv() | skriv_alle() |
hent_navn() | hent_alle_navn() eller navneliste() |
antall() | totalt_antall() |
antall_spill() | totalt_antall_spill() |
__str__ | skriv_alle(), som bruker print(e) |
Regelen koster ingenting og gjør besvarelsen lettere å lese. Ta den med deg til eksamen.
Vis en ytterklasse som skriver ut, teller og summerer ved å spørre underobjektene — i begge samlingsformer.
Klassen Rett vet alt om én rett. Kantine vet ingenting om retter, bare at den har mange av dem:
class Rett:
def __init__(self, navn, pris, solgte):
self._navn = navn self._pris = pris self._solgte = solgte
def hent_navn(self):
return self._navn
def hent_solgte(self):
return self._solgte
def omsetning(self):
return self._pris * self._solgte
def __str__(self):
return self._navn + ": " + str(self._pris) + " kr x" + str(self._solgte)
class Kantine:
def __init__(self, navn):
self._navn = navn self._retter = {}
def legg_til(self, rett):
self._retter[rett.hent_navn()] = rett
def skriv_alle(self):
print("Meny i", self._navn)
for navn in self._retter:
print(" -", self._retter[navn])
def samlet_omsetning(self):
total = 0 for navn in self._retter:
total = total + self._retter[navn].omsetning()
return total
def antall_populaere(self, grense):
antall = 0 for navn in self._retter:
if self._retter[navn].hent_solgte() >= grense:
antall = antall + 1 return antall
k = Kantine("Fredrikke")
k.legg_til(Rett("Fiskesuppe", 89, 14))
k.legg_til(Rett("Lapskaus", 79, 22))
k.legg_til(Rett("Vafler", 35, 40))
k.skriv_alle()
print("Omsetning:", k.samlet_omsetning())
print("Minst 20 solgt:", k.antall_populaere(20))Utskrift:
Meny i Fredrikke
- Fiskesuppe: 89 kr x14
- Lapskaus: 79 kr x22
- Vafler: 35 kr x40
Omsetning: 4384
Minst 20 solgt: 2Tre metoder, tre former for delegering.
skriv_alle lar print kalle rettens egen __str__. Kantina vet ikke hvordan en rett vises.
samlet_omsetning spør hver rett hva den har omsatt for. Kantina regner ikkepris * solgte selv — det ville dessuten krevd to aksessorer den ikke har.
antall_populaere spør etter én verdi og sammenligner. Grensen kommer inn som parameter, så metoden kan gjenbrukes med andre tall.
Slik ville sensor sett på det: hver av de tre metodene er sitt eget poeng, og
delegeringen teller for seg. Hadde samlet_omsetning skrevetself._retter[navn]._pris * self._retter[navn]._solgte, ville summen blitt riktig — men to ganger feilkode #11 i én linje.
Merk navnene: omsetning() på retten, samlet_omsetning() på kantina.
Containerens metode heter aldri det samme som elementets.
— naturlig pausepunkt —
En resirkuleringsstasjon har containere.
Hver container har et materiale og en fyllingsgrad i prosent.
a) Skriv Container med aksessor for fyllingen og en __str__ på formenglass: 40 %.
b) Skriv Stasjon med containerne i en liste, sett_inn(self, c), en
metode som skriver ut alle, og en som teller hvor mange som er minst 80 prosent fulle.
c) Hva skal utskriftsmetoden hete, og hvorfor ikke skriv?
Hva skriver programmet ut?
class Spill:
def __init__(self, tittel, maks):
self._tittel = tittel self._maks = maks
def hent_tittel(self):
return self._tittel
def hent_maks(self):
return self._maks
def __str__(self):
return self._tittel + "(" + str(self._maks) + ")"
class Klubb:
def __init__(self):
self._spill = {}
def legg_til(self, s):
self._spill[s.hent_tittel()] = s
def skriv_alle(self):
for tittel in self._spill:
print(self._spill[tittel])
def totalt_antall_plasser(self):
total = 0 for tittel in self._spill:
total = total + self._spill[tittel].hent_maks()
return total
k = Klubb()
k.legg_til(Spill("Kolonier", 4))
k.legg_til(Spill("Sjakk", 2))
k.legg_til(Spill("Ordjakt", 6))
k.skriv_alle()
print(k.totalt_antall_plasser())Løkke 4 — Objekter som peker på hverandre (~15 min)
Den siste formen: et underobjekt som selv holder en referanse oppover eller
sidelengs.
Et spill har en eier som er et medlem. En konto har en eier som er en person. En sal kan holde en referanse til senteret den hører til. Da kan du spørre deg nedover — og av og til oppover — gjennom modellen med ett punktum om gangen.
self._eier = medlem — en instansvariabel som peker på et helt objekt av en annen klasse.Verdien kommer inn i konstruktøren som en vanlig parameter, og settes med self._ som alt annet. Forskjellen er bare at den peker på et objekt i stedet for et tall eller en tekst.
Gevinsten er at modellen holder seg i takt: spør duself._eier.hent_navn(), får du navnet slik det er nå. Lagrer du navnet som tekst i konstruktøren, blir det foreldet i det øyeblikket eieren skifter navn.
Det er en av de faste vurderingene i Oppgave 4.
klubb.finn_spill("Sjakk").hent_eier().hent_navn() — hvert punktum er ett steg videre i modellen.Les det fra venstre: finn spillet, spør det om eieren, spør eieren om navnet.
Formen er ryddig så lenge kjeden er kort, og den er fasitens egen stil. Blir den lang, eller kan et ledd gi None, sett resultatet i en hjelpevariabel underveis:
spill = klubb.finn_spill("Sjakk")
if spill != None:
print(spill.hent_eier().hent_navn())Blokken er et utdrag og er merket som ikke-kjørbar.
Det er like god uttelling, og det er lettere å spore.
Bygg en modell der ytterklassen holder mange underobjekter, og hvert underobjekt peker på et objekt av en tredje klasse.
En brettspillklubb: klubben har mange spill, og hvert spill har én eier, som er et medlem.
class Medlem:
def __init__(self, navn):
self._navn = navn self._kvelder = 0
def hent_navn(self):
return self._navn
def hent_kvelder(self):
return self._kvelder
def registrer_kveld(self):
self._kvelder = self._kvelder + 1
def __str__(self):
return self._navn + " (" + str(self._kvelder) + " kvelder)"
class Spill:
def __init__(self, tittel, eier):
self._tittel = tittel self._eier = eier
def hent_tittel(self):
return self._tittel
def hent_eier(self):
return self._eier
def __str__(self):
return self._tittel + " - eier: " + self._eier.hent_navn()
class Klubb:
def __init__(self):
self._spill = {}
def legg_til(self, s):
self._spill[s.hent_tittel()] = s
def finn(self, tittel):
if tittel in self._spill:
return self._spill[tittel]
return None
def skriv_alle(self):
for tittel in self._spill:
print(self._spill[tittel])
ada = Medlem("Ada")
nora = Medlem("Nora")
k = Klubb()
k.legg_til(Spill("Kolonier", ada))
k.legg_til(Spill("Sjakk", nora))
k.legg_til(Spill("Ordjakt", ada))
ada.registrer_kveld()
ada.registrer_kveld()
k.skriv_alle()
print(k.finn("Sjakk").hent_eier())
print(k.finn("Kolonier").hent_eier().hent_kvelder())
print(k.finn("Yatzy"))Utskrift:
Kolonier - eier: Ada
Sjakk - eier: Nora
Ordjakt - eier: Ada
Nora (0 kvelder)
2
NoneFire ting er verdt å merke seg.
Ada er eier av to spill, men det finnes bare ett Ada-objekt. Begge spillene
holder en referanse til det samme medlemmet. Registrerer Ada en kveld, ser begge spillene det med én gang — det er referansesemantikken fra
kap. 5.4 i praksis.
Spill.__str__ spør eieren om navnet i stedet for å lagre det. Skifter Ada
navn, følger utskriften med av seg selv.
Den lenkede kjeden k.finn("Kolonier").hent_eier().hent_kvelder() går tre
steg gjennom modellen: klubb, spill, medlem. Den er lesbar så lenge du vet at hvert ledd gir et objekt.
Og k.finn("Yatzy") gir None — trygt, fordi finn har sjekken sin. Hadde
du skrevet k.finn("Yatzy").hent_eier(), ville programmet stoppet medAttributeError: 'NoneType' object has no attribute 'hent_eier'.
Slik ville sensor sett på det: dette er en typisk 15–18-poengs deloppgave.
Poengene ligger på de tre konstruktørene, ordboka med riktig nøkkel, objektreferansen i Spill, oppslaget med sjekk, return None, delegeringen iskriv_alle, og de to __str__. Mangler én, står de andre.
Se på modellen fra Eksempel 4, der ada eier både «Kolonier»
og «Ordjakt».
a) Hvor mange Medlem-objekter finnes etter at de tre spillene er lagt til?
b) ada.registrer_kveld() kalles én gang. Hva girk.finn("Ordjakt").hent_eier().hent_kvelder()?
c) En medstudent foreslår at Spill skal lagre `self.eiernavn =
eier.hentnavn()` i stedet for hele objektet. Hva går tapt?
Les spesifikasjonen:
> Et treningssenter har et navn og flere saler. Hver sal har et navn, en
> kapasitet og en samling påmeldte medlemmer. Hvert medlem har et navn og et
> antall besøk som starter på null. En sal kan melde på et medlem, men bare hvis
> det er ledig plass — og påmeldingen skal samtidig registrere et besøk på
> medlemmet. Senteret kan legge til en sal, finne en sal på navn, skrive ut alle
> salene, og si hvor mange påmeldinger det er til sammen.
a) Skriv de tre klassene.
b) Test: to saler, tre medlemmer, ett avvist forsøk, og alle fire metodene iSenter i bruk.
c) Hvorfor heter metoden som teller påmeldinger i Senter ikke det samme som
metoden i Sal?
En medstudent har skrevet:
class Bok:
def __init__(self, tittel, forfatter):
self._tittel = tittel self._forfatter = forfatter
class Bibliotek:
def __init__(self):
self._boker = {}
def legg_til(self, bok):
self._boker[bok._tittel] = bok
def finn(self, tittel):
return self._boker[tittel]
def skriv_alle(self):
for tittel in self._boker:
print(self._boker[tittel]._tittel, "av", self._boker[tittel]._forfatter)a) Klassene har tre feil. Hvilke?
b) Gi et konkret moteksempel på den som stopper programmet.
c) Skriv den rettede versjonen.
Fem feil koster nesten alle poengene som mistes på samarbeidende klasser:
- Rådata i stedet for objektreferanser. append(bok.hent_tittel()) mister
metodene, og alle senere deloppgaver faller.
- #14 — oppslag uten sjekk. return self._d[navn] stopper programmet på en
nøkkel som ikke finnes. Standardformen er sjekk, returner, return None.
- #11 — e._x i en delegeringsløkke. Den vanligste formen feilkoden tar når
to klasser samarbeider. Spør i stedet for å grave.
- Ikke delegere. Ytterklassen som regner e._pris * e._antall selv,
dupliserer logikk som hører hjemme i underklassen.
- Feil samlingstype. «Finne på navn» betyr ordbok; «gå gjennom alle» betyr
liste. Feil valg gjør neste deloppgave unødig tung.
Og én navnefeil: containerens metode som heter det samme som elementets.skriv som kaller skriv ser ut som en metode som kaller seg selv. Brukskriv_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.
Forbindelsen der ett objekt er knyttet til mange av en annen klasse: et bibliotek med bøker, et senter med saler, en klubb med medlemmer.
I koden er den én linje i konstruktøren — self._elementer = [] eller {} — og én legg_til-metode.
Hver eneste Oppgave 4 i de seks moderne settene har minst én. Kjenn den igjen i teksten på ordene «flere», «en samling», «alle» og flertallsform.
Klassen hvis viktigste jobb er å holde mange objekter av en annen klasse.
Den kjennes igjen på fire trekk: en samling i konstruktøren, en legg_til, en eller flere metoder som løper gjennom samlingen, og som regel en finn.
Hver Oppgave 4 har minst én. Mønsteret er derfor verdt å kunne i fingrene — det er de samme fire bitene hvert år.
Samlingen holder objektene, ikke opplysninger om dem:self._boker.append(bok), ikke self._boker.append(bok.hent_tittel()).
Lagrer du rådata, mister du metodene. Biblioteket kan ikke lenger be en bok om å låne seg ut, skrive seg selv eller si hvem forfatteren er.
Det er den dyreste enkeltbeslutningen i Oppgave 4, fordi den slår ut i hver eneste senere deloppgave.
self._saler[sal.hent_navn()] = sal.Containeren bestemmer ikke navnet, den spør objektet. Alternativetsal._navn er feilkode #11, og det er den vanligste formen feilkoden tar når to klasser samarbeider.
Formen er også tryggere: endrer Sal hvordan navnet lagres, merker containeren ingenting.
Standardformen på finn, i tre linjer: sjekk med if navn in self._d:,return self._d[navn], og return None ellers.
None er det ærlige svaret når det ikke finnes noe. Den som kaller, tester medif svar != None:.
Alternativene — en tom tekst eller -1 — kan forveksles med ekte data. Det er feilkode #10, sentinel-kollisjon.
Å slå opp eller legge til på en nøkkel som ikke er opprettet.
To former: d[navn] uten sjekk, som stopper med KeyError og nøkkelen i meldingen. Og d[navn].append(x) når nøkkelen ikke finnes — samme feil, samme løsning.
Løsningen er alltid en if-sjekk foran: if navn in d: for oppslag,if navn not in d: og d[navn] = [] for oppbygging.
Feilmeldingen du får når du kaller en metode på et søkeresultat som var None.
AttributeError: 'NoneType' object has no attribute 'hent_navn' betyr nesten alltid: et finn-kall fant ingenting, og du glemte å sjekke svaret.
Motgiften er if svar != None: hos den som kaller. Den er billig og fjerner hele feilklassen.
At ytterklassen ber underobjektet gjøre jobben, i stedet for å gjøre den selv.
Kjennetegnet i koden er en løkke der kroppen er ett kall på underobjektet, og ingen felt hentes ut med understrek.
Gevinsten er den samme som ved innkapsling: endrer underklassen hvordan den regner eller viser seg, merker ytterklassen ingenting.
Løkka over en ordbok gir nøklene, ikke objektene:
for n in self._d:
print(self._d[n])Blokken er et utdrag og er merket som ikke-kjørbar.
Skriver du for e in self._d: print(e), får du nøklene skrevet ut som tekst — en klassisk sporingsfelle. Objektet må hentes med et oppslag.
En metode kaller aldri en metode med samme navn — verken på seg selv eller på et underobjekt.
Elementet har skriv(), containeren har skriv_alle(). Elementet harantall(), containeren har totalt_antall().
To grunner: navnene beskriver ulike jobber, og en metode som ser ut som om den kaller seg selv, ligner på rekursjon, som er utenfor pensum i dette emnet.
Mønsteret for å summere noe underobjektene vet:
def totalt_antall(self):
total = 0 for e in self._elementer:
total = total + e.antall()
return totalBlokken er en mal og er merket som ikke-kjørbar.
Tre plasseringer avgjør: total = 0 før løkka, + inne i den, return
etter den. Det er nøyaktig samme mønster som i
kap. 3.1 — bare med et metodekall i stedet for en verdi.
self._eier = medlem — en instansvariabel som peker på et objekt av en annen klasse, satt i konstruktøren som en vanlig parameter.Spør du self._eier.hent_navn(), får du navnet slik det er nå. Lagrer du navnet som tekst, blir det foreldet i det øyeblikket eieren skifter navn.
Det er en av de faste vurderingene i Oppgave 4, og det er lett å bomme på under tidspress.
klubb.finn("Sjakk").hent_eier().hent_navn() — hvert punktum er ett steg videre i modellen.Les fra venstre: finn spillet, spør det om eieren, spør eieren om navnet.
Kan et ledd gi None, del kjeden opp med en hjelpevariabel og en sjekk. Det er like god uttelling, og det er lettere å spore.
En utleie har mange stativer, og hvert stativ har mange sykler. Ytterklassen har en ordbok, hvert stativ en liste.
Det er helt vanlig i Oppgave 4, og 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 å spørre «hvor mange står på Blindern?».
Et medlem kan stå i medlemslista og i ventelista samtidig, og et medlem kan være eier av to spill.
Da finnes det fortsatt bare ett objekt. Endring gjennom den ene veien sees gjennom den andre, og det er som regel nettopp det du vil.
len() teller referanser, ikke objekter. Spør en oppgave «hvor mange objekter er laget?», teller du klassenavn med parentes.
En ordbok kan aldri ha to like nøkler. Legger du inn samme navn to ganger,
overskrives det første objektet uten en advarsel.
Er det et problem, må oppgaven si hva som skal skje — og da er teller-idiomet eller en if-sjekk foran innsettingen svaret.
Er det ikke et problem, er overskrivingen ofte nettopp det som er ønsket: den nyeste opplysningen gjelder.
for e in self._elementer: print(e._navn) virker, men er feilkode #11 — og den koster på tre måter.Den bryter konvensjonen sensor ser etter. Den dupliserer visningen, så to steder må endres når formatet skal endres. Og den binder ytterklassen til underklassens innside, slik at ingen av dem kan endres uavhengig.
Alternativet er én linje kortere: print(e).
Ordboka husker den rekkefølgen nøklene ble opprettet i — ikke når de sist ble endret, og ikke etter størrelse.
En delegeringsløkke over en ordbok skriver derfor ut i innsettingsrekkefølge, og det er verdt å vite når du skal spore en utskrift.
Skal noe sorteres, må det gjøres eksplisitt. Bokas stil er en eksplisitt finn-beste-løkke, som kommer i kap. 6.3.
def legg_til(self, bok): — ikke def legg_til(self, tittel, forfatter):.Containeren skal ikke lage objektene; det gjør den som kaller. Da kan det samme objektet legges i to samlinger, og den som kaller, beholder en referanse til det.
Begge former forekommer i settene, men objektvarianten er den fasiten bruker, og den er mer fleksibel.
Ett spørsmål: skal noe slås opp på et navn eller en kode?
Ja: ordbok. Nei: liste.
Er du usikker, velg liste og skriv en finn-metode med en løkke. Det gir full uttelling, og det er lettere å skrive riktig når klokka går. Ordboka er en optimalisering, ikke et krav.
meld_paa legger medlemmet i lista og ber medlemmet registrere et besøk.Det er helt vanlig i Oppgave 4: én handling i domenet berører to objekter. Begge delene hører hjemme i else-grenen, slik at et avvist forsøk ikke registrerer noe.
Kjenn den igjen i oppgaveteksten på ordet «samtidig» eller på to setninger som hører sammen: «en sal kan melde på et medlem, og påmeldingen skal registrere et besøk».
Skriv alltid en test som treffer både det som finnes og det som ikke finnes:print(b.finn("Sult")) og print(b.finn("Naboer")).
På eksamen er det ingen som kjører koden din, men vanen er verdt å ha likevel:
den tvinger deg til å tenke gjennom hva som skjer når søket mislykkes.
Det er nettopp den situasjonen if navn in ...-sjekken finnes for.
Hver container-klasse i Oppgave 4 består av de samme fire bitene:
1. samlingen i konstruktøren,
2. en legg_til,
3. et oppslag med sjekk (hvis ordbok),
4. én eller flere metoder som delegerer.
Kan du de fire, kan du skrive halve Oppgave 4 uten å tenke — og bruke tiden på algoritme-metoden i stedet, som er der de vanskelige poengene ligger.
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.