Tilbake
7.6

7.6 DRILL — Robusthet: ukjent rekkefølge, tomtilfeller og ingen duplikater (fullscore)

De eksplisitte fullscore-kravene samlet: finn kolonneindeks dynamisk, håndtér tomme/ikke-funnet-tilfeller, og unngå duplikater i resultatet.

75 min
12 oppgaver
DRILLRobusthetukjent rekkefølgetomtilfelleringen duplikater (fullscore)
Din fremgang i kapitlet
0 / 12 oppgaver
Forkunnskaper — sist du var her

Robusthet er ikke nytt stoff, men gamle grep brukt bevisst. Her er de tre, ferdig oppfrisket.

1. Kolonnen funnet ved navn (kap. 3.2):

overskrift = ['oppgave', 'navn', 'timer']
print(overskrift.index('timer'))

Utskrift:

2

2. None som startverdi i «finn beste» (kap. 7.4):

beste = None
for t in []:
    beste = t
print(beste)

Utskrift:

None

3. Medlemskapstesten in (kap. 3.1):

resultat = ['Hedda', 'Ivar']
print('Hedda' in resultat)
print('Nils' in resultat)

Utskrift:

True
False

I tillegg forutsetter kapitlet kap. 7.4, som ga oss analysemønstrene robustheten legges oppå.

Løkke 1 — de fire kravene (~13 min)

Robusthet høres ut som noe stort og vagt. På eksamen er det fire helt konkrete ting, og de kommer igjen i sett etter sett.

Robusthet — hva sensor mener med ordet

Robusthet er ikke «feilfri kode». Det er at funksjonen gir et fornuftig svar også når dataene ikke er slik du håpet.

Fire situasjoner, og en robust funksjon har et bevisst svar på hver av dem:

1. Kolonnene kommer i en annen rekkefølge enn i eksempelfila.
2. Det finnes ingen rader, eller ingen rad som passer søket.
3. Den samme verdien dukker opp flere ganger, men skal bare med én gang.
4. Radene kommer i tilfeldig rekkefølge, ikke gruppert eller sortert.

Merk formuleringen «bevisst svar». Det er ikke noe krav om at koden skal gjette hva brukeren mente — det er et krav om at den ikke krasjer og ikke lyver.

📜Robusthetssjekklista — fire spørsmål før du leverer

Bruk to minutter på denne lista når analysedeloppgavene er skrevet. Den er verdt mer per minutt enn noe annet du kan gjøre på slutten.

1. Kolonne: står det et talltegn som rad[2] i koden min? Bytt det med tabell[0].index('timer').
2. Tomt: hva returnerer funksjonen hvis tabellen ikke har datarader, eller hvis ingen rad passer? Skal den gi [], None, -1 eller {}?
3. Duplikater: kan den samme verdien komme flere ganger i resultatet mitt? Skal den det?
4. Rekkefølge: antar koden min at like rader står ved siden av hverandre, eller at tabellen er sortert?

Og en femte, som ikke er kode: skriv én setning om hver antakelse du har måttet gjøre. Sensorregelen er uttalt — kort og klart er bedre enn detaljert.

Robusthet er ikke `try/except` overalt
try/except fra kap. 4.3 hører hjemme rundt fil-I/O og konvertering — der feilen kommer utenfra og du ikke kan forutse den.

De fire kravene i dette kapitlet løses derimot med vanlige if-tester, fordi du vet nøyaktig hva som kan gå galt:

if len(tabell) <= 1:
    return None

Blokken over er et utdrag, ikke et program.

En besvarelse som pakker hele analysen i try: … except: … for å «være robust», skjuler feilene i stedet for å håndtere dem — og en naken except: fanger til og med skrivefeilene dine. Det gir dårligere uttelling enn en if-test som sier hva den passer på.

Regelen: try mot det uforutsigbare, if mot det du kan se komme.

✏️Eksempel 1: den samme funksjonen, før og etter

Her er en analysefunksjon som virker på dugnadslista i borettslaget, og den samme funksjonen gjort robust. Hva er forskjellen, linje for linje?

Slik ser fila ut:

Innholdet i dugnad.txt:

oppgave;navn;timer
maling;Hedda;3.5
rydding;Ivar;2.0
maling;Ivar;1.5
beplantning;Hedda;4.0
rydding;Nils;2.5
maling;Hedda;2.0

Før — virker på akkurat disse dataene:

def flest_timer(tabell):
    beste = tabell[1]
    for rad in tabell[1:]:
        if rad[2] > beste[2]:
            beste = rad
    return beste[1]

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(flest_timer(tabell))

Utskrift:

Hedda

Koden gir riktig svar her, og den har riktig struktur — én løkke, én sammenligning. Det gir god uttelling. Men den har tre svakheter, og alle tre er nevnt i oppgavetekstene i arkivet.

Etter — de tre grepene lagt inn:

def flest_timer(tabell):
    if len(tabell) <= 1:
        return None
    k_timer = tabell[0].index('timer')
    k_navn = tabell[0].index('navn')
    beste = None
    beste_verdi = None
    for rad in tabell[1:]:
        if beste_verdi is None or rad[k_timer] > beste_verdi:
            beste = rad[k_navn]
            beste_verdi = rad[k_timer]
    return beste

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(flest_timer(tabell))
print(flest_timer([['oppgave', 'navn', 'timer']]))

Utskrift:

Hedda
None

De tre grepene, og hva hvert av dem koster:

GrepLinjerHva det redder
tabell[0].index(...)2riktig svar når kolonnene kommer i annen rekkefølge
if len(tabell) <= 1: return None2et krasj på tom tabell
beste_verdi is None2riktig svar når alle timetallene er negative eller null

Seks linjer til sammen. Det er hele avstanden fra «virker» til «full pott».
Slik ville sensor sett på det: den første versjonen er en solid midtnivåbesvarelse — den ville fått god uttelling, kanskje 70–80 % av deloppgavens poeng, fordi strukturen er riktig. Den andre får alt.
Legg merke til at forskjellen ikke er dyktighet i programmering. Den er en sjekkliste, kjørt i to minutter på slutten.

📝Oppgave 1

(Innstegsoppgave — les koden.) Her er en funksjon som skal returnere navnet på den som har jobbet færrest timer:

def faerrest_timer(tabell):
    beste = tabell[1]
    for rad in tabell[1:]:
        if rad[2] < beste[2]:
            beste = rad
    return beste[1]

a) Hvilke av de fire robusthetskravene bryter den?

b) Hva skjer hvis tabell bare inneholder overskriftsraden?

📝Oppgave 2
Eksamensnivå

Skriv om funksjonen fra oppgave 1 slik at den oppfyller begge kravene den brøt. Den skal fortsatt returnere navnet, og den skal returnere None når det ikke finnes noen rader å velge mellom.

Løkke 2 — krav 1: finn kolonnen dynamisk (~13 min)

Det første kravet er også det som nevnes oftest i oppgavetekstene: «du kan ikke anta at kolonnene kommer i denne rekkefølgen».

Fullscore-krav 1: dynamisk kolonneoppslag

Kolonnenummeret skal leses ut av overskriftsraden, aldri skrives inn i koden.

Riktig: k = tabell[0].index('timer'), deretter rad[k].

Galt: rad[2].

Grunnen er ikke elegant kode. Grunnen er at oppgavetekstene sier det rett ut, og at fila på det virkelige eksamenssettet kan ha kolonnene i en annen rekkefølge enn eksempelet i oppgaven.

Feilen har et nummer: #4 — anta fast kolonne- eller radrekkefølge. Den er stum. Programmet krasjer ikke; det leser bare feil kolonne og svarer med full selvtillit.

`tabell[0].index(navn)` — og hva den krasjer på
.index(v) gir posisjonen til den første forekomsten av v i lista, og krasjer med ValueError hvis v ikke finnes.

overskrift = ['oppgave', 'navn', 'timer']
print(overskrift.index('navn'))
print(overskrift.index('timer'))

Utskrift:

1
2

To ting å vite:

- Den er følsom for store og små bokstaver: 'Timer' finnes ikke når overskriften sier 'timer'.
- Den ser bare på eksakte treff; 'time' finner ikke 'timer'.

Er du usikker på om kolonnen finnes — for eksempel fordi brukeren har skrevet inn navnet — skal du teste først. Det er neste definisjon.

`if navn in overskrift:` — sjekk før oppslag

Medlemskapstesten før .index(...) gjør forskjellen mellom en feilmelding og et fornuftig svar.

def kolonne(tabell, navn):
    if navn not in tabell[0]:
        return -1
    return tabell[0].index(navn)

t = [['oppgave', 'navn', 'timer']]
print(kolonne(t, 'timer'))
print(kolonne(t, 'lonn'))

Utskrift:

2
-1

-1 er den vanlige returverdien for «ikke funnet» når svaret er en posisjon — det er den samme konvensjonen som i sekvensielt søk, kap. 6.2. Den som kaller, tester if k == -1: og gir brukeren beskjed.

Når trenger du testen? Når kolonnenavnet kommer utenfra — fra brukeren, fra en meny, eller fra en annen fil. Er navnet skrevet inn i din egen kode, er .index alene nok: da er det din skrivefeil som gir ValueError, og den vil du gjerne se.

✏️Eksempel 2: den samme funksjonen på to filer

Vis at en funksjon med dynamisk kolonneoppslag gir riktig svar på både dugnad.txt og dugnad-omstokket.txt, som har de samme dataene i en annen kolonnerekkefølge.

De to filene:

Innholdet i dugnad.txt:

oppgave;navn;timer
maling;Hedda;3.5
rydding;Ivar;2.0
maling;Ivar;1.5
beplantning;Hedda;4.0
rydding;Nils;2.5
maling;Hedda;2.0

Innholdet i dugnad-omstokket.txt:

navn;timer;oppgave
Hedda;3.5;maling
Ivar;2.0;rydding
Ivar;1.5;maling
Hedda;4.0;beplantning

Innlesingen finner tallkolonnen ved navn, så den virker på begge:

def les_dugnad(filnavn):
    f = open(filnavn, 'r')
    overskrift = f.readline().strip().split(';')
    k_timer = overskrift.index('timer')
    tabell = [overskrift]
    for linje in f:
        felt = linje.strip().split(';')
        felt[k_timer] = float(felt[k_timer])
        tabell.append(felt)
    f.close()
    return tabell

def sum_timer(tabell):
    k = tabell[0].index('timer')
    total = 0
    for rad in tabell[1:]:
        total = total + rad[k]
    return total

print(sum_timer(les_dugnad('dugnad.txt')))
print(sum_timer(les_dugnad('dugnad-omstokket.txt')))

Utskrift:

15.5
11.0

De to filene har ulikt antall rader, så tallene er forskjellige — men begge er riktige, og det er poenget: funksjonen leste riktig kolonne i begge tilfellene uten å vite noe om rekkefølgen.

Hva den hardkodede varianten ville gitt på den omstokkede fila:

tabell = [['navn', 'timer', 'oppgave'],
          ['Hedda', 3.5, 'maling'],
          ['Ivar', 2.0, 'rydding']]
print(tabell[1][2])
print(tabell[1][tabell[0].index('timer')])

Utskrift:

maling
3.5

rad[2] gir oppgavenavnet 'maling' der koden ventet et timetall. Summeringen ville gitt TypeError: unsupported operand type(s) for +: 'int' and 'str' — her krasjer det, men bare fordi kolonnen tilfeldigvis er tekst. Hadde de to kolonnene begge vært tall, ville du fått et svar. Feil svar, uten et pip.

Slik ville sensor sett på det: dette er den ene tingen oppgavetekstene i arkivet nevner mest eksplisitt. To linjer med .index(...) per funksjon er et av de billigste poengene i hele settet.

📝Oppgave 3
Eksamensnivå, sjanger G
«Skriv timer_for(tabell, navn) som returnerer det totale antallet timer en bestemt person har lagt ned. Både navnekolonnen og timekolonnen skal finnes ved navn, og funksjonen skal returnere 0 hvis personen ikke står i lista.»
📝Oppgave 4
Eksamensnivå, sjanger G
«Skriv sum_kolonne(tabell, kolonnenavn) som summerer en tallkolonne brukeren har oppgitt navnet på. Finnes ikke kolonnen, skal funksjonen returnere None i stedet for å krasje.»

Test både med 'timer' og med et navn som ikke finnes.

Løkke 3 — krav 2: tomme og ikke-funnet-tilfeller (~13 min)

Det andre kravet handler om ett spørsmål: hva skal funksjonen svare når det ikke finnes noe svar? Det finnes fire vanlige svar, og valget mellom dem er en del av oppgaven.

— naturlig pausepunkt etter denne løkka —

Fullscore-krav 2: tomtilfeller og «ikke funnet»

To situasjoner som ser like ut, men ikke er det:

- Tomt datagrunnlag: tabellen har ingen datarader. Da finnes det ingenting å analysere.
- Ikke funnet: tabellen har rader, men ingen som passer søket.

Begge skal ha et bevisst svar, og svaret skal stå i koden eller i én setning i besvarelsen.

Det som ikke er akseptabelt, er at funksjonen krasjer med IndexError, ValueError eller ZeroDivisionError. En krasjende deloppgave gir null, uansett hvor riktig resten av koden er.

Returverdier for «ingenting»: `[]`, `None`, `-1`, `{}`

Fire konvensjoner, valgt ut fra hva funksjonen ellers returnerer:

Funksjonen returnererVed ingenting
ei liste[] — den tomme lista
en verdi eller en radNone
en posisjon i ei liste-1
en ordbok{} — den tomme ordboka

Regelen bak: behold returtypen. Returnerer funksjonen ei liste i normaltilfellet, skal den også returnere ei liste når svaret er tomt — da kan den som kaller, skrive for x in svar: uten å teste først.
Blander du typene — [] i det ene tilfellet og None i det andre — tvinger du den som kaller til å teste to ganger, og det er nettopp der de neste feilene oppstår.
0 er ikke i tabellen. 0 er et tall, og det brukes bare når null faktisk er et gyldig svar, som i «antall timer for en som ikke møtte».

Mønstre som takler tomt av seg selv

Tre av bokas mønstre er robuste helt uten ekstra linjer, og det er verdt å vite hvilke:

tabell = [['navn', 'timer']]
beste = None
for rad in tabell[1:]:
    beste = rad
print(beste)

d = {}
for rad in tabell[1:]:
    d[rad[0]] = 1
print(d)

resultat = []
for rad in tabell[1:]:
    resultat.append(rad[0])
print(resultat)

Utskrift:

None
{}
[]

«Finn beste» med None, telleren og oppsamlingslista gir alle riktig tomsvar av seg selv, fordi løkkekroppen aldri kjører.

De som ikke gjør det, er snarveiene og divisjonene: max([]) og min([]) gir ValueError, sum(x) / len(x) gir ZeroDivisionError, og sortert[0] gir IndexError. Bruker du dem, må du teste.

✏️Eksempel 3: fire funksjoner, fire tomsvar

Skriv fire analysefunksjoner på dugnadslista, hver med sin returtype, og vis hva hver av dem svarer på en tabell uten datarader.

1. Returnerer ei liste — tomsvaret er []:

def alle_som_deltok(tabell):
    k = tabell[0].index('navn')
    resultat = []
    for rad in tabell[1:]:
        if rad[k] not in resultat:
            resultat.append(rad[k])
    return resultat

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(alle_som_deltok(tabell))
print(alle_som_deltok([['oppgave', 'navn', 'timer']]))

Utskrift:

['Hedda', 'Ivar', 'Nils']
[]

2. Returnerer en verdi — tomsvaret er None:

def snitt_timer(tabell):
    k = tabell[0].index('timer')
    total = 0
    antall = 0
    for rad in tabell[1:]:
        total = total + rad[k]
        antall = antall + 1
    if antall == 0:
        return None
    return total / antall

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(snitt_timer(tabell))
print(snitt_timer([['oppgave', 'navn', 'timer']]))

Utskrift:

2.5833333333333335
None

3. Returnerer en posisjon — tomsvaret er -1:

def finn_rad(tabell, navn):
    k = tabell[0].index('navn')
    for i in range(1, len(tabell)):
        if tabell[i][k] == navn:
            return i
    return -1

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(finn_rad(tabell, 'Nils'))
print(finn_rad(tabell, 'Kari'))

Utskrift:

5
-1

4. Returnerer en ordbok — tomsvaret er {}:

def timer_per_person(tabell):
    k_navn = tabell[0].index('navn')
    k_timer = tabell[0].index('timer')
    d = {}
    for rad in tabell[1:]:
        d[rad[k_navn]] = d.get(rad[k_navn], 0) + rad[k_timer]
    return d

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(timer_per_person(tabell))
print(timer_per_person([['oppgave', 'navn', 'timer']]))

Utskrift:

{'Hedda': 9.5, 'Ivar': 3.5, 'Nils': 2.5}
{}

Slik ville sensor sett på det: legg merke til at bare én av de fire trengte en egen if-test. De tre andre er robuste fordi mønsteret er det — lista, ordboka og løkka som returnerer -1 til slutt.

Det er en god grunn til å lære mønstrene slik boka skriver dem: robustheten er allerede bygget inn, og du slipper å huske den.

Antakelsen å skrive ned for funksjon 3: returnerer -1 ved «ikke funnet», og finner første treff hvis navnet står flere ganger. To ting, én setning.

📝Oppgave 5
Eksamensnivå, sjanger G
«Skriv lengste_okt(tabell) som returnerer hele raden for den lengste enkeltøkta. Funksjonen skal returnere None hvis det ikke finnes noen økter, og den skal virke uansett kolonnerekkefølge.»

Test på en full tabell, på en tabell med bare overskriftsrad, og på en tabell der alle timetallene er 0.

📝Oppgave 6
Eksamensnivå, sjanger G
«Skriv snitt_per_oppgave(tabell) som returnerer {oppgave: gjennomsnittlig timer}. Funksjonen skal (a) finne begge kolonnene ved navn, (b) returnere en tom ordbok hvis det ikke finnes rader, og (c) aldri dele på null.»

Skriv én setning om hvorfor deloppgave (c) er umulig å bryte i denne løsningen.

Løkke 4 — krav 3: ingen duplikater (~13 min)

Det tredje kravet står ordrett i flere oppgavetekster: «listen skal ikke inneholde duplikater». Det er en enkelt linje kode, og den er verdt hele forskjellen mellom riktig og galt svar.

Fullscore-krav 3: ingen duplikater i resultatet

Bygger du ei resultatliste med append i en løkke, kommer den samme verdien med én gang per rad som passer. Er det tre rader med Hedda, står Hedda tre ganger.

Formuleringene i oppgaveteksten som utløser kravet:

- «uten duplikater» / «ingen gjentakelser»
- «alle forskjellige deltakere»
- «hvilke oppgaver som er utført» (i motsetning til «alle utførte oppgaver»)

Er formuleringen tvetydig, velger du selv og skriver én setning om valget. Men vær oppmerksom: duplikatkravet er så ofte nevnt eksplisitt at det lønner seg å lese oppgaveteksten en ekstra gang før du antar.

`if x not in resultat:` — den eksplisitte formen

Testen står rett før append, og den er hele grepet:

navn = ['Hedda', 'Ivar', 'Hedda', 'Nils', 'Ivar']
resultat = []
for n in navn:
    if n not in resultat:
        resultat.append(n)
print(resultat)

Utskrift:

['Hedda', 'Ivar', 'Nils']

Fordelen ved denne formen er at rekkefølgen bevares: navnene kommer i den rekkefølgen de dukket opp første gang. Det er ofte det oppgaven vil ha, og det er lett å forklare.

Formen er også lett å lese for sensor: én linje som sier nøyaktig hva den passer på.

`set(...)` — samlingen uten duplikater

En mengde (set) er en samling der hver verdi finnes høyst én gang. set(liste) fjerner duplikatene i ett kall.

navn = ['Hedda', 'Ivar', 'Hedda', 'Nils', 'Ivar']
print(len(set(navn)))
print(sorted(set(navn)))

Utskrift:

3
['Hedda', 'Ivar', 'Nils']

To ting du må vite før du bruker den:

- Rekkefølgen i en mengde er ikke noe du kan stole på. Derfor skriver boka alltid sorted(set(...)) når resultatet skal vises eller sammenlignes — det gir ei liste i fast, alfabetisk rekkefølge.
- Mengden er en annen type enn ei liste. Skal funksjonen returnere ei liste, må du konvertere: list(set(x)) eller sorted(set(x)).

set er en snarvei på lik linje med max og sorted, og begge former gir full pott.

`sorted(set(...))` — når rekkefølgen skal være fast

Kombinasjonen som gir både «ingen duplikater» og «forutsigbar rekkefølge» på én linje:

oppgaver = ['maling', 'rydding', 'maling', 'beplantning', 'rydding']
print(sorted(set(oppgaver)))

Utskrift:

['beplantning', 'maling', 'rydding']

Bruk den når resultatet skal vises for en bruker eller sammenlignes med et fasitsvar — da er alfabetisk rekkefølge både pent og etterprøvbart.

Bruk den eksplisitte if x not in resultat-formen når rekkefølgen fra dataene er en del av svaret: «i den rekkefølgen de meldte seg».

Merk at sorted på blandede typer krasjer: sorted({1, 'a'}) gir TypeError. Det er sjelden et problem i dette faget, siden kolonnene er ensartede.

✏️Eksempel 4: hvem har gjort hva — med og uten duplikater
«Skriv deltakere_per_oppgave(tabell) som returnerer {oppgave: [navn, …]} — hvem som har vært med på hver oppgave. Den samme personen skal bare stå én gang per oppgave.»
Uten duplikatsjekk — først, for å se problemet:

def deltakere_naiv(tabell):
    k_opp = tabell[0].index('oppgave')
    k_navn = tabell[0].index('navn')
    d = {}
    for rad in tabell[1:]:
        if rad[k_opp] not in d:
            d[rad[k_opp]] = []
        d[rad[k_opp]].append(rad[k_navn])
    return d

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(deltakere_naiv(tabell))

Utskrift:

{'maling': ['Hedda', 'Ivar', 'Hedda'], 'rydding': ['Ivar', 'Nils'], 'beplantning': ['Hedda']}

Hedda står to ganger under maling, fordi hun har to malelinjer i fila. Det er riktig hvis spørsmålet var «alle malingsøktene», og galt hvis det var «hvem som har malt».

Med duplikatsjekk — to tester i stedet for én:

def deltakere_per_oppgave(tabell):
    k_opp = tabell[0].index('oppgave')
    k_navn = tabell[0].index('navn')
    d = {}
    for rad in tabell[1:]:
        opp = rad[k_opp]
        if opp not in d:
            d[opp] = []
        if rad[k_navn] not in d[opp]:
            d[opp].append(rad[k_navn])
    return d

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(deltakere_per_oppgave(tabell))

Utskrift:

{'maling': ['Hedda', 'Ivar'], 'rydding': ['Ivar', 'Nils'], 'beplantning': ['Hedda']}

De to testene gjør helt forskjellige ting, og det er verdt å si det høyt:

TestHva den passer på
if opp not in d:at den indre lista finnes før append kalles
if rad[k_navn] not in d[opp]:at navnet ikke allerede står i den lista

Dropper du den første, får du KeyError. Dropper du den andre, får du duplikater. De erstatter ikke hverandre.
Snarveien med set, som gir det samme:
tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
k_opp = tabell[0].index('oppgave')
k_navn = tabell[0].index('navn')
d = {}
for rad in tabell[1:]:
    d.setdefault(rad[k_opp], set()).add(rad[k_navn])
for opp in sorted(d):
    print(opp.ljust(14), sorted(d[opp]))

Utskrift:

beplantning    ['Hedda']
maling         ['Hedda', 'Ivar']
rydding        ['Ivar', 'Nils']

set() er den tomme mengden, og .add(v) legger til uten å lage duplikater. Legg merke til at utskriften bruker sorted(...) — mengdens egen rekkefølge er ikke noe å stole på.

Slik ville sensor sett på det: begge gir full pott. Den eksplisitte formen er lettest å lese og forklare; set-formen er kortest. Det som ikke gir full pott, er å levere den naive versjonen når oppgaveteksten sa «uten duplikater» — og den setningen står i oppgaveteksten oftere enn noen annen robusthetsformulering.

📝Oppgave 7
Eksamensnivå, sjanger G
«Skriv oppgaver_for(tabell, navn) som returnerer ei liste med alle oppgavene en bestemt person har vært med på — hver oppgave bare én gang, i alfabetisk rekkefølge. Returnér ei tom liste hvis personen ikke finnes.»
📝Oppgave 8
Eksamensnivå, sjanger B

Hva skriver programmet ut? Skriv alle tre linjene, og forklar den tredje med én setning.

navn = ['Hedda', 'Ivar', 'Hedda', 'Nils']
a = []
for n in navn:
    if n not in a:
        a.append(n)
print(a)
print(len(set(navn)))
print(sorted(set(navn)) == sorted(a))

Løkke 5 — krav 4: vilkårlig rekkefølge på radene (~12 min)

Det fjerde kravet er det som er lettest å bryte uten å merke det, fordi eksempelfila i oppgaven som regel er pent ordnet: «bilene passerer i tilfeldig rekkefølge», «linjene er ikke sortert».

Fullscore-krav 4: ikke anta noe om rekkefølgen

Koden din skal gi samme svar uansett hvilken rekkefølge radene kommer i.

To antakelser som bryter kravet, og som begge er lette å gjøre:

- «Like rader står ved siden av hverandre.» Da kan du telle med en variabel som nullstilles når nøkkelen skifter. Kommer radene blandet, teller du feil.
- «Tabellen er sortert.» Da kan du ta den første raden som den minste. Er den ikke sortert, er svaret tilfeldig.

Mønstrene boka bruker — ordbok-telleren, grupperingen og «finn beste» — er alle uavhengige av rekkefølgen. Det er en av grunnene til at de er valgt.

Testen du kan gjøre selv på to minutter: stokk om radene i testdataene dine og kjør på nytt. Får du et annet svar, har du en skjult antakelse.

✏️Eksempel 5: telling med og uten rekkefølgeantakelse

To måter å telle hvor mange økter hver person har hatt. Den ene virker bare hvis radene er gruppert per person. Vis forskjellen.

Den skjøre varianten — teller opp, og skriver ut når navnet skifter:

def okter_skjor(tabell):
    k = tabell[0].index('navn')
    d = {}
    forrige = None
    antall = 0
    for rad in tabell[1:]:
        if rad[k] != forrige:
            if forrige is not None:
                d[forrige] = antall
            forrige = rad[k]
            antall = 0
        antall = antall + 1
    if forrige is not None:
        d[forrige] = antall
    return d

sortert = [['oppgave', 'navn', 'timer'],
           ['maling', 'Hedda', 3.5],
           ['beplantning', 'Hedda', 4.0],
           ['maling', 'Hedda', 2.0],
           ['rydding', 'Ivar', 2.0],
           ['maling', 'Ivar', 1.5]]
print(okter_skjor(sortert))

Utskrift:

{'Hedda': 3, 'Ivar': 2}

På den grupperte tabellen ser den helt riktig ut: Hedda 3, Ivar 2.

Den samme funksjonen på de samme dataene, blandet:

def okter_skjor(tabell):
    k = tabell[0].index('navn')
    d = {}
    forrige = None
    antall = 0
    for rad in tabell[1:]:
        if rad[k] != forrige:
            if forrige is not None:
                d[forrige] = antall
            forrige = rad[k]
            antall = 0
        antall = antall + 1
    if forrige is not None:
        d[forrige] = antall
    return d

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(okter_skjor(tabell))

Utskrift:

{'Hedda': 1, 'Ivar': 2, 'Nils': 1}

Nå er svaret feil: Hedda står med 1 økt i stedet for 3, fordi tellingen ble nullstilt hver gang navnet skiftet, og bare den siste klyngen ble lagret.

Ordbok-telleren, som ikke bryr seg om rekkefølgen:

def okter_robust(tabell):
    k = tabell[0].index('navn')
    d = {}
    for rad in tabell[1:]:
        d[rad[k]] = d.get(rad[k], 0) + 1
    return d

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(okter_robust(tabell))
sortert = [['oppgave', 'navn', 'timer'],
           ['maling', 'Hedda', 3.5],
           ['beplantning', 'Hedda', 4.0],
           ['maling', 'Hedda', 2.0],
           ['rydding', 'Ivar', 2.0],
           ['maling', 'Ivar', 1.5],
           ['rydding', 'Nils', 2.5]]
print(okter_robust(sortert))

Utskrift:

{'Hedda': 3, 'Ivar': 2, 'Nils': 1}
{'Hedda': 3, 'Ivar': 2, 'Nils': 1}

Samme svar begge veier, uansett rekkefølge — og funksjonen er fem linjer i stedet for tolv.

Slik ville sensor sett på det: den skjøre varianten er ikke bare mer utsatt, den er også lengre og vanskeligere å lese. Det er sjelden en avveining mellom robusthet og enkelhet i dette faget: det robuste mønsteret er nesten alltid også det korteste.

Regelen å ta med seg: når du kjenner igjen at du holder styr på «den forrige raden», skal en varsellampe lyse. Ordboka gjør den bokføringen for deg.

📝Oppgave 9
Eksamensnivå

Denne funksjonen skal returnere den korteste økta:

def korteste(tabell):
    k = tabell[0].index('timer')
    return tabell[1][k]

a) Hvilken antakelse gjør den om dataene?

b) Skriv den om slik at den virker uansett rekkefølge, og returnerer None på en tom tabell.

📝Oppgave 10
Eksamensnivå, sjanger G
«Radene i dugnadslista kommer i tilfeldig rekkefølge. Skriv okter_per_person_og_oppgave(tabell) som returnerer {navn: {oppgave: antall}} — en ordbok av ordbøker.»

Test at svaret er det samme når radene stokkes om.

Løkke 6 — alle fire kravene i én funksjon (~11 min)

Til slutt setter vi de fire sammen — og legger til den femte tingen, som ikke er kode.

Antakelsen skrevet ned

Er oppgaveteksten uklar, velger du selv og skriver én setning om valget i besvarelsen.

Fire antakelser som går igjen, med ferdige formuleringer:

- «Antar at det bare finnes én vinner; ved likhet returneres den som står først i tabellen.»
- «Antar at et tomt felt betyr ikke registrert, ikke null — raden holdes utenfor snittet.»
- «Antar at navn sammenlignes eksakt, så store og små bokstaver skiller.»
- «Antar at kolonnenavnene i overskriftsraden er som oppgitt, men ikke at rekkefølgen er det.»

Sensorregelen som gjelder: svar kort og klart. Er svaret uklart eller lengre enn nødvendig, trekker det ned. Én setning per antakelse er nok — det er ingen premie for et avsnitt.

Femten sekunder per setning er forskjellen mellom «feil» og «riktig gitt en dokumentert antakelse».

✏️Eksempel 6: dugnadsrapporten som tåler alt

Skriv en analysefunksjon som oppfyller alle fire kravene samtidig: «aktive_deltakere(tabell, minst) skal returnere navnene på alle som har lagt ned minst det oppgitte antallet timer til sammen, uten duplikater, i alfabetisk rekkefølge — og ei tom liste hvis ingen kvalifiserer.»

Funksjonen bygges i to steg: først telleordboka, så filtreringen.

def timer_per_person(tabell):
    k_navn = tabell[0].index('navn')
    k_timer = tabell[0].index('timer')
    d = {}
    for rad in tabell[1:]:
        d[rad[k_navn]] = d.get(rad[k_navn], 0) + rad[k_timer]
    return d

def aktive_deltakere(tabell, minst):
    d = timer_per_person(tabell)
    resultat = []
    for navn in d:
        if d[navn] >= minst:
            resultat.append(navn)
    return sorted(resultat)

tabell = [['oppgave', 'navn', 'timer'],
          ['maling', 'Hedda', 3.5],
          ['rydding', 'Ivar', 2.0],
          ['maling', 'Ivar', 1.5],
          ['beplantning', 'Hedda', 4.0],
          ['rydding', 'Nils', 2.5],
          ['maling', 'Hedda', 2.0]]
print(aktive_deltakere(tabell, 3.0))
print(aktive_deltakere(tabell, 100.0))
print(aktive_deltakere([['oppgave', 'navn', 'timer']], 1.0))

Utskrift:

['Hedda', 'Ivar']
[]
[]

Hedda har 9.5 timer og Ivar 3.5; Nils har 2.5 og faller utenfor. Med grensen 100 kvalifiserer ingen, og med tom tabell finnes ingen.

De fire kravene, punkt for punkt:

KravHvor det er oppfylt
1 — dynamisk kolonnetabell[0].index('navn') og .index('timer') i timer_per_person
2 — tomtilfelletom tabell gir tom ordbok, tom løkke og [] — helt gratis
3 — ingen duplikaterordboka har én nøkkel per person, så duplikater er umulige
4 — vilkårlig rekkefølgetelleren og filtreringen bryr seg ikke om rekkefølgen

Legg merke til hvordan krav 3 løses her: ikke med en not in-test, men ved at strukturen gjør duplikater umulig. En ordbok har én nøkkel per verdi, og løkka går over nøklene. Det er den ryddigste måten å oppfylle kravet på, og den er verdt å se etter — kan du velge en struktur som gjør feilen umulig, slipper du å teste for den.
Antakelsen å skrive ned: «minst» tolkes som >=, altså at grensen selv teller med. Sier oppgaven «mer enn», er det >. Én setning.
Slik ville sensor sett på det: alle fire kravene er dekket, og bare ett av dem krevde egne linjer. Det er den korteste veien til full pott — og den er tilgjengelig for alle som kjenner mønstrene.

📝Oppgave 11
Eksamensnivå, sjanger G
«Skriv mest_aktive(tabell) som returnerer navnet på den som har lagt ned flest timer til sammen. Funksjonen skal finne kolonnene ved navn, virke uansett radrekkefølge, og returnere None hvis det ikke finnes noen deltakere.»

Skriv i tillegg én setning om hva funksjonen gjør ved likhet.

📝Oppgave 12
Eksamensnivå, sjanger G

Her er tre funksjoner fra en besvarelse som virker på eksempelfila:

def deltakere(tabell):
    resultat = []
    for rad in tabell[1:]:
        resultat.append(rad[1])
    return resultat

def toppdeltaker(tabell):
    beste = tabell[1]
    for rad in tabell[1:]:
        if rad[2] > beste[2]:
            beste = rad
    return beste[1]

def snitt(tabell):
    total = 0
    for rad in tabell[1:]:
        total = total + rad[2]
    return total / (len(tabell) - 1)

a) Gå gjennom robusthetssjekklista og finn bruddene i hver funksjon.

b) Skriv alle tre om.

API- og konstruksjonsliste

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 Norges teknisk-naturvitenskapelige universitet. Dette er ikke offisielt studiemateriell. Les mer.