Tilbake
6.2

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.

60 min
8 oppgaver
Flere samarbeidende klasseren-til-mange
Din fremgang i kapitlet
0 / 8 oppgaver

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 Sult

2. 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 ikke

3. 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.

📜Klassemodellering-malen, steg 3: multiplisitet blir samling

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.

Liste av objektreferanser
self._elementer = [] i konstruktøren, self._elementer.append(obj) i
legg_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.

Ordbok av objektreferanser
self._elementer = {} i konstruktøren, og innsettingen bruker elementets egen
aksessor 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:

2

Utskriften 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.

✏️Eksempel 1: En-til-mange, med liste og med ordbok

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 None

De 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.

📝Oppgave 1

(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.

Oppslagsmetoden

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
None

if 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.

✏️Eksempel 2: Oppslag, med og uten sjekk

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 Basseng

Legg merke til if sal != None: hos den som kaller. Uten den ville
sal.hent_kapasitet() stoppet programmet på det andre navnet, og siste linje i feilmeldingen ville vært
AttributeError: '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.

📝Oppgave 2
Eksamensnivå, sjanger F

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) og
antall_sykler(self).

b) Skriv klassen Utleie med ordboka, legg_til(self, stativ) og
finn(self, navn).

c) Test: to stativer, tre sykler fordelt på dem, ett oppslag som lykkes og
ett som ikke gjør det.

📝Oppgave 3
Eksamensnivå, sjanger A

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.

📜Delegerings-malen

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 total

Blokken 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.

✏️Eksempel 3: Delegering over liste og over ordbok

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: 2

Tre 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 ikke
pris * 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 skrevet
self._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 —

📝Oppgave 4
Eksamensnivå, sjanger F

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å formen
glass: 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?

📝Oppgave 5
Eksamensnivå, sjanger A

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.

Objektreferanse mellom to klasser
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 du
self._eier.hent_navn(), får du navnet slik det er . 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.

Lenket metodekall
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.

✏️Eksempel 4: Tre klasser som peker på hverandre

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
None

Fire 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 med
AttributeError: '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 i
skriv_alle, og de to __str__. Mangler én, står de andre.

📝Oppgave 6
Eksamensnivå, sjanger B

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 gir
k.finn("Ordjakt").hent_eier().hent_kvelder()?

c) En medstudent foreslår at Spill skal lagre `self.eiernavn =
eier.hent
navn()` i stedet for hele objektet. Hva går tapt?

📝Oppgave 7
Eksamensnivå, sjanger F

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 i
Senter i bruk.

c) Hvorfor heter metoden som teller påmeldinger i Senter ikke det samme som
metoden i Sal?

📝Oppgave 8
Eksamensnivå, sjanger I

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.

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.

En-til-mange

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.

Container-klasse

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.

Objektreferanse i en samling

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.

Nøkkel fra objektets egen aksessor
self._saler[sal.hent_navn()] = sal.

Containeren bestemmer ikke navnet, den spør objektet. Alternativet
sal._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.

Oppslagsmetoden

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 med
if svar != None:.

Alternativene — en tom tekst eller -1 — kan forveksles med ekte data. Det er feilkode #10, sentinel-kollisjon.

Feilkode #14

Å 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.

«NoneType has no attribute»

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.

Delegering

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.

Delegering over en ordbok

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.

Navneregelen for containere

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 har
antall(), 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.

Akkumulator med delegering

Mønsteret for å summere noe underobjektene vet:

    def totalt_antall(self):
        total = 0 for e in self._elementer:
            total = total + e.antall()
        return total

Blokken 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.

Objektreferanse mellom to klasser
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 . 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.

Lenket metodekall
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.

To nivåer av en-til-mange

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?».

Samme objekt i flere samlinger

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.

Overskriving i en ordbok

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.

Hvorfor ikke grave i underobjektet
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).

Rekkefølge i en ordbok

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.

`legg_til` tar objektet, ikke verdiene
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.

Å velge samlingstype under tidspress

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.

Metoden som gjør to ting
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».

Å teste begge utfall

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.

Fire biter som gjentar seg

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.

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.