8.3 DRILL — Fillesing til nøstet dict med filtrering
Full drill på L-sjangeren: les→bygg nøstet dict→filtrer→skriv, med roterende datainnhold.
Dette er drillkapitlet for sjanger L — fillesing til nøstet ordbok, altså «les fila linje for linje og bygg datastrukturen». Sjangeren forekommer i 8 av 8 gjennomgåtte avsluttende sett (100 %) og teller 8–12 poeng, oftest delt i tre: (a) les fila til en nøstet ordbok, (b) skriv en struktur til fil, (c) filtrer og returner en ny ordbok. Prioriteten er den høyeste boka har: dette må sitte.
Grunnen til at et helt kapittel går til drill, er at oppgaven er nesten uforandret fra år til år. Bare innpakningen roterer: målestasjoner, folkeregister, landstatistikk, karakterlister, vannprøver. Separatoren varierer (semikolon, komma, faste kolonner), og noen filer har header-linje. Alt annet er identisk. Klarer du å kjenne igjen mønsteret bak innpakningen, er dette de mest forutsigbare poengene på hele eksamen.
Kapitlet har tre deler: én løsningsoppskrift på seks steg, ett gjennomarbeidet eksamenscase med sensorkommentar ved hvert steg, og ti oppgaver på eksamensnivå med roterende data. Du besvarer alt som fritekst i Inspera, UiOs digitale eksamenssystem, uten hjelpemidler og uten å kunne kjøre koden — derfor er poenget her å få oppskriften ned i fingrene, ikke å lese den.
Om delpoeng: sensor gir uttelling for hvert steg i oppskriften for seg. Riktig with open, riktig løkke og riktig oppbygging av strukturen belønnes selv om separatoren er feil eller en .strip() mangler. Å få alle detaljer 100 % riktig på en hjelpemiddelfri eksamen er nesten umulig — riktig struktur er det som teller.
Forkunnskaper — sist du var her
Drillen forutsetter kap. 8.1 (lesing) og kap. 8.2 (skriving og filtrering). Her er de tre grepene du kommer til å bruke i hver eneste oppgave, oppfrisket med kjørt utskrift.
1. Lesing til nøstet ordbok. Strip før split, konverter med én gang, return utenfor løkka.
def les_stasjoner(filnavn):
data = {}
with open(filnavn) as infile:
for line in infile:
navn, temp, nedbor = line.strip().split(';')
data[navn] = {'temp': float(temp), 'nedbor': int(nedbor)}
return data
print(len(les_stasjoner('stasjoner.txt')))Utskrift:
42. Den indre ordboka må opprettes når flere linjer deler samme ytre nøkkel:
data = {}
for stasjon in ['Vest', 'Aust', 'Vest']:
if stasjon not in data:
data[stasjon] = {'antall': 0}
data[stasjon]['antall'] += 1
print(data)Utskrift:
{'Vest': {'antall': 2}, 'Aust': {'antall': 1}}3. Filtrering bygger en ny ordbok — aldri ny = data:
def storre_enn(d, grense):
ny = {}
for k, v in d.items():
if v > grense:
ny[k] = v
return ny
d = {'Kvitheia': 1180, 'Sandvollen': 760}
print(storre_enn(d, 1000))
print(d)Utskrift:
{'Kvitheia': 1180}
{'Kvitheia': 1180, 'Sandvollen': 760}Er noe av dette uklart, les de to forrige kapitlene først — drillen forklarer ikke konstruksjonene på nytt. Feilmeldingene som dukker opp underveis, er katalogisert i kap. 2.2.
Les oppgaveteksten, og gjør så dette, i rekkefølge. Hvert steg svarer til ett spørsmål du besvarer ved å se på fila som er gjengitt i oppgaven.
1. Hva er den ytre nøkkelen? Det som identifiserer raden: stasjonsnavn, ID, land, kandidatnummer. Er det bare én linje per nøkkel, kan hele den indre ordboka settes inn i én tilordning. Har samme nøkkel flere linjer, trenger du et nøkkelnivå mer (typisk dato) og en if nøkkel not in data:-sjekk.
2. Er det en header-linje? Står det kolonnenavn på den første linja, hopper du over den med infile.readline() før løkka — eller bruker den som kilde til feltnavnene. Er det ingen header, skal readline() ikke være der.
3. Hva er separatoren? Semikolon gir .split(';'), komma gir .split(','), mellomrom eller TAB gir bar .split(), faste kolonner gir line[a:b]. Les det ut av fila; ikke gjett.
4. Hvilke felt er tall, og hvilke er tekst? Konverter tallfeltene med int eller float med én gang, inne i løkka, og la tekstfeltene være. Da inneholder strukturen ferdige verdier.
5. Del (b) — skriving: with open(filnavn, 'w') as outfile:, én write per linje, hver linje bygget som f-streng og avsluttet med '\n'. Skriver du en header, må leseren senere hoppe over den.
6. Del (c) — filtrering: ny = {}, løkke over .items(), if-betingelse, innsetting, og return ny utenfor løkka. Argumentet skal være urørt.
Skjelettet er alltid dette, og det er verdt å skrive ned først, før du fyller inn detaljene:
def les(filnavn):
data = {}
with open(filnavn) as infile:
for line in infile:
felt = line.strip().split(';')
data[felt[0]] = {}
return data
print(len(les('stasjoner.txt')))Utskrift:
4Skjelettet gir uttelling i seg selv. Fyller du inn de indre feltene feil, mister du ett eller to poeng — glemmer du skjelettet, mister du oppgaven.
.split() på en semikolonfil gir én bit per linje, og neste linje krasjer med IndexError: list index out of range. Sjekk fila.#7 — glemt .strip() eller glemt readline(). Uten .strip() drar siste felt linjeskiftet med seg. På tallfelt merker du det ikke, fordi int og float tåler blanktegn; på tekstfelt får du en verdi som 'regn\n' som aldri er lik 'regn'. Uten readline() havner kolonnenavnet i en int() og gir ValueError med kolonnenavnet sitert.
#20 — mutasjon i stedet for ny ordbok. ny = data er aliasing, ikke kopiering: du endrer originalen. Skriv alltid ny = {}.
Glemt '\n' i write. Alt havner på én linje, og feltene gror sammen: 1180Sandvollen. write legger aldri til linjeskift.
Og én som ikke har eget nummer, men koster like mye: return inne i løkka. Da returnerer funksjonen etter første linje, og du får en ordbok med ett element. return data skal stå på samme innrykk som data = {}.
Rekursjon er utenfor pensum i IN1900. Fristes du til å lese fila «rekursivt» ved å la en funksjon kalle seg selv for hver linje, er svaret nei: det er uønsket i fasit og gir dårligere eller ingen uttelling. Løkka er den forventede løsningen — alltid.
Det gjennomarbeidede caset (~20 min)
Under står en komplett, tredelt sjanger-L-oppgave av samme type og vekt som på et virkelig sett, med sensorkommentar ved hvert steg. Les den som en modell: det er nøyaktig denne formen svarene dine skal ha.
Oppgaven (10 poeng). Fila vind.txt har en header-linje og deretter én linje per måling, med feltene stasjon;dato;vind;retning der vindstyrken er i meter per sekund. Samme stasjon har flere målinger.
Innholdet i vind.txt:
stasjon;dato;vind;retning
Kvitheia;2025-01-04;12.4;SV
Kvitheia;2025-01-05;7.1;NV
Ospedalen;2025-01-04;18.9;S
Ospedalen;2025-01-05;21.3;SV
Nordvika;2025-01-04;5.2;Na) (4 p) Skriv les_vind(filnavn) som returnerer en nøstet ordbok med stasjon som ytre nøkkel, dato som neste, og {'vind': ..., 'retning': ...} innerst.
b) (3 p) Skriv skriv_sterk_vind(data, filnavn, grense) som lagrer alle målingene over en gitt vindstyrke til fil, med header-linja stasjon;dato;vind.
c) (3 p) Skriv snitt_per_stasjon(data) som returnerer en ny ordbok fra stasjonsnavn til gjennomsnittlig vindstyrke, uten å endre argumentet.
Denne fila kan du lage selv på to sekunder — det er dataene løsningen under leser.
Del (a), 4 poeng: les_vind(filnavn) skal returnere en nøstet ordbok med stasjon → dato → {'vind': ..., 'retning': ...}.
Gå gjennom oppskriften. Ytre nøkkel er stasjonen, og samme stasjon har flere linjer — altså trengs et nøkkelnivå til (datoen) og en not in-sjekk. Det er header-linje, så readline() skal med. Separatoren er semikolon. vind er tall, retning er tekst.
def les_vind(filnavn):
data = {}
with open(filnavn) as infile:
infile.readline()
for line in infile:
stasjon, dato, vind, retning = line.strip().split(';')
if stasjon not in data:
data[stasjon] = {}
data[stasjon][dato] = {'vind': float(vind), 'retning': retning}
return data
data = les_vind('vind.txt')
print(data['Kvitheia'])
print(data['Ospedalen']['2025-01-05']['vind'])
print(len(data))Utskrift:
{'2025-01-04': {'vind': 12.4, 'retning': 'SV'}, '2025-01-05': {'vind': 7.1, 'retning': 'NV'}}
21.3
3Sensorkommentar, steg for steg:
- data = {} og with open(filnavn) as infile: — 1 p. Rene rutinelinjer, men de gis poeng, og de er de eneste du kan skrive før du har forstått fila.
- infile.readline() før løkka — 0,5 p. Glemmer du den, får du ValueError: could not convert string to float: 'vind', og du mister dette halve poenget. Resten av oppgaven står.
- line.strip().split(';') — 1 p. Riktig separator er det halve; .strip() er nødvendig her fordi retning er tekstfeltet til slutt på linja, og uten strip blir verdien 'SV\n'.
- if stasjon not in data: data[stasjon] = {} — 1 p. Dette er kjernen. Skriver du i stedet data[stasjon][dato] = ... direkte, krasjer programmet med KeyError på første linje.
- Innsetting på tre nivåer med float(vind) og retning som tekst — 0,5 p.
- return data utenfor løkka — inngår i det første poenget, men står return inne i løkka, mister du hele strukturpoenget: funksjonen returnerer etter første måling.
Hva som er godt nok: hadde du skrevet data[stasjon][dato] = float(vind) og droppet retningen, ville du fått 3 av 4 — strukturen er riktig, ett felt er glemt.
Del (b), 3 poeng: skriv_sterk_vind(data, filnavn, grense) skal lagre målingene over grense til fil med header. Del (c), 3 poeng: snitt_per_stasjon(data) skal returnere en ny ordbok med gjennomsnittlig vindstyrke per stasjon.
def skriv_sterk_vind(data, filnavn, grense):
with open(filnavn, 'w') as outfile:
outfile.write('stasjon;dato;vind\n')
for stasjon, dager in data.items():
for dato, verdier in dager.items():
if verdier['vind'] > grense:
outfile.write(f'{stasjon};{dato};{verdier["vind"]:.1f}\n')Del (c). Aggregeringsmønsteret: for hver ytre nøkkel, summer over de indre og del på antall. len(dager) er antall målinger for den stasjonen.
def snitt_per_stasjon(data):
ny = {}
for stasjon, dager in data.items():
total = 0
for dato in dager:
total += dager[dato]['vind']
ny[stasjon] = total / len(dager)
return nyKjørt sammen med les_vind fra del (a), med grensen 10,0 m/s:
data = les_vind('vind.txt')
skriv_sterk_vind(data, 'sterk.txt', 10.0)
with open('sterk.txt') as infile:
for line in infile:
print(line, end='')
snitt = snitt_per_stasjon(data)
for stasjon in snitt:
print(f'{stasjon:11s}{snitt[stasjon]:6.2f}')Utskrift:
stasjon;dato;vind
Kvitheia;2025-01-04;12.4
Ospedalen;2025-01-04;18.9
Ospedalen;2025-01-05;21.3
Kvitheia 9.75
Ospedalen 20.10
Nordvika 5.20Sensorkommentar del (b): with open(filnavn, 'w') (1 p) · header skrevet én gang før løkkene (0,5 p) · to nøstede løkker som når ned til målingene (1 p) · f-streng som slutter på '\n' (0,5 p). Glemmer du '\n', ligger alle målingene på én linje — det koster det halve poenget, ikke mer.
Sensorkommentar del (c): ny = {} og return ny utenfor løkka (1 p) · summeringsløkke over de indre nøklene (1 p) · deling på len(dager), altså antall målinger for den stasjonen (1 p). Den vanligste feilen her er å dele på len(data), antall stasjoner — det gir tre i stedet for to, og koster ett poeng.
Merk hva som ikke kreves: ingen main-blokk, ingen brukerinput, ingen kommentarer utover eventuelle antakelser. Tolv linjer kode for ti poeng.
Oppgaver: samme mønster, ny innpakning (~30 min for oppgave 1–5)
— naturlig pausepunkt —
Alle oppgavene under er på eksamensnivå. Datainnholdet roterer bevisst, og separatoren og header-linja varierer. Bruk oppskriftens seks spørsmål på hver oppgave før du skriver en linje kode. Filene kan du lage selv i en tekstredigerer — innholdet står i oppgaven.
Et folkeregisterutdrag ligger i personer.txt med header-linje og semikolon som separator.
Innholdet i personer.txt:
id;navn;postnr;fodselsaar
1042;Ingrid Hauge;5003;1998
1043;Mathias Lind;7014;1985
1044;Sofie Berge;5003;2001
1045;Jonas Rud;9012;1972a) Skriv les_personer(filnavn) som returnerer en nøstet ordbok med id-en som heltallsnøkkel og en indre ordbok med feltnavnene fra header-linja. Navnet skal være tekst, postnummer og fødselsår heltall.
b) Skriv ut den indre ordboka for id 1044, navnet og fødselsåret til 1043, og antall personer.
Fila land.txt er kommaseparert og har ingen header. Feltene er land,befolkning,bnp_per_innbygger.
Innholdet i land.txt:
Alveria,4820000,52100
Brenland,1290000,38400
Corvia,17650000,9800
Dunmark,610000,71200a) Skriv les_land(filnavn) som returnerer en nøstet ordbok fra landnavn til {'folketall': ..., 'bnp': ...} med heltall som verdier.
b) Skriv rikeste(data) som returnerer navnet på landet med høyest BNP per innbygger.
c) Vis den indre ordboka for Corvia, resultatet av rikeste og antall land.
(Midtveisnivå, sjanger A — kodesporing, altså «hva skriver dette programmet ut?».) Programmet leser land.txt fra forrige oppgave. Hva skriver det ut? Skriv utskriften nøyaktig, med mellomrom og linjeskift slik terminalen viser den.
data = {}
with open('land.txt') as infile:
for line in infile:
felt = line.strip().split(',')
data[felt[0]] = int(felt[1])
n = 0
for navn in data:
if data[navn] > 1000000:
n += 1
print(navn, end=' ')
print()
print(n, len(data))readline().strip().split(sep)[1:] svaret.Oppgave 2 testet det motsatte: en fil uten header, med en annen separator. Å skrive readline() av vane er en reell feil som koster en datarad.
Oppgave 3 var ikke en kodeskrivingsoppgave i det hele tatt, men en sporingsoppgave om samme kode. Slik ser fillesing ut på midtveiseksamen: du skal ikke skrive innlesingen, du skal si nøyaktig hva den skriver ut. Da er end=' ' og det tomme print() det egentlige temaet.
Bruk vind.txt og les_vind fra det gjennomarbeidede caset.
a) Skriv maks_per_stasjon(data) som returnerer en ny ordbok fra stasjonsnavn til den høyeste vindstyrken som er målt på stasjonen. Argumentet skal ikke endres.
b) Vis resultatet for hele fila.
Gitt en ordbok fra person-id til {'navn': ..., 'fodselsaar': ...}, som den les_personer fra oppgave 1 lager.
a) Skriv skriv_unge(data, filnavn, aar) som lagrer alle personer født etter aar til fil, med header-linja id;navn og deretter én semikolonseparert linje per person.
b) Kall funksjonen med aar = 1990 på Ingrid Hauge (id 1042, født 1998), Mathias Lind (1043, 1985) og Sofie Berge (1044, 2001), og les fila tilbake.
Oppgave 6–10: tredelte oppgaver og de vanskeligere variantene (~25 min)
— naturlig pausepunkt —
De neste fem oppgavene ligner mest på det du møter til slutt i et sett: flere deler som bygger på hverandre, én «finn feilen», og én kolonnefil. Har du de fem første inne, er dette samme mønster med flere nivåer.
Fila karakterer.txt har header-linje og feltene kandidat;emne;karakter;poeng. Samme kandidat kan ha flere emner.
Innholdet i karakterer.txt:
kandidat;emne;karakter;poeng
K1201;IN1900;B;71
K1202;IN1900;D;48
K1201;MAT1100;C;60
K1203;IN1900;A;88a) Skriv les_karakterer(filnavn) som returnerer en nøstet ordbok med kandidatnummer som ytre nøkkel, emnekode som neste, og {'karakter': ..., 'poeng': ...} innerst, der poengsummen er et heltall.
b) Skriv bestatt(data, grense) som returnerer en ny ordbok med bare de resultatene som har minst grense poeng. Kandidater uten kvalifiserte resultater skal ikke være med i det hele tatt.
c) Skriv skriv(data, filnavn) som lagrer en slik struktur til fil med linjer kandidat;emne;karakter, og kjør hele kjeden med grensen 60.
Programmet skal lese vind.txt fra caset til en ordbok med stasjon og dato som nøkler og vindstyrken som verdi. Det krasjer på første linje.
data = {}
with open('vind.txt') as infile:
for line in infile:
felt = line.split()
data[felt[0]][felt[1]] = float(felt[2])
print(data)a) Hvilken feilmelding kommer, og hvorfor?
b) Programmet har tre feil. Nevn alle tre, og skriv den rettede koden.
(Bør kjenne til, sjanger L — fast-bredde-felt.) Fila kolonner.txt har ingen separator; feltene står i faste kolonner. Navnet er i posisjon 0–11, nedbøren i 12–17 og temperaturen i 18–24. Hver linje er 25 tegn.
Innholdet i kolonner.txt:
Kvitheia 1180 7.9
Sandvollen 760 5.4
Ospedalen 1420 8.6a) Skriv koden som leser fila til en nøstet ordbok fra stasjonsnavn til {'nedbor': ..., 'temp': ...}, med heltall og flyttall.
b) Skriv ut den indre ordboka for Sandvollen og summen av nedbøren for Ospedalen og Kvitheia.
Bruk personer.txt fra oppgave 1.
a) Skriv per_postnr(filnavn) som returnerer en ordbok fra postnummer (som tekst) til en liste med navnene på personene som bor der.
b) Vis hele ordboka og antall personer i postnummer 5003.
Fila avlop.txt har header-linje og feltene stasjon;dato;ph;temp. Samme stasjon har flere prøver.
Innholdet i avlop.txt:
stasjon;dato;ph;temp
Vest;2025-05-02;7.4;11.2
Vest;2025-05-03;6.9;12.0
Aust;2025-05-02;8.1;10.4a) Skriv les_avlop(filnavn) som returnerer en nøstet ordbok stasjon → dato → {'ph': ..., 'temp': ...} med flyttall.
b) Skriv sure_dogn(data, grense) som returnerer en ny struktur med bare prøvene der pH er lavere enn grense, uten å endre argumentet.
c) Skriv skriv_snitt(data, filnavn) som lagrer gjennomsnittlig pH per stasjon til fil, med header-linja stasjon;snitt_ph og to desimaler.
d) Kjør kjeden med grensen 7,0 og vis resultatene.
beste eller total mellom stasjonene, er svaret galt for alle unntatt den første.Oppgave 5 testet skriving med betingelse: header én gang, if inne i løkka, '\n' på hver linje.
Oppgave 6 og 10 var de fulle tredelte oppgavene, den formen sjanger L har når den er verdt 10–12 poeng. Kjernen i begge er at filteret må opprette den ytre nøkkelen bare ved treff — ellers får du med kandidater og stasjoner som ikke har noe å vise til.
Oppgave 7 testet feilkatalogen baklengs: gitt et program som krasjer, finn feilene. De tre du skal kjenne igjen på et blunk er separator, header og manglende indre ordbok.
Oppgave 8 var kolonnefila — nivå 3, «bør kjenne til». Metoden gir uttelling selv med bommede posisjoner.
Oppgave 9 testet at verdien i en ordbok gjerne kan være en liste, og at .append er forskjellig fra tilordning. Grupperingsmønsteret dukker opp hver gang oppgaveteksten sier «alle X som hører til Y».
Begrepsbank
Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.
Fillesing til nøstet ordbok: oppgaven der du leser en tekstfil linje for linje og bygger en datastruktur. Den forekommer i 8 av 8 gjennomgåtte avsluttende sett og teller 8–12 poeng, oftest delt i tre: (a) les til ordbok, (b) skriv struktur til fil, (c) filtrer og returner en ny ordbok. Innpakningen roterer år for år, programmeringsøvelsen er den samme.
Det feltet som identifiserer raden: stasjonsnavn, ID, land, kandidatnummer. Har hver nøkkel én linje, settes hele den indre ordboka inn i én tilordning. Har samme nøkkel flere linjer, trengs et nøkkelnivå til (typisk dato) og en if nøkkel not in data:-sjekk. Dette valget avgjør hele resten av koden.
Er den første linja kolonnenavn, hoppes den over med infile.readline() før løkka — eller den brukes som kilde til feltnavnene med readline().strip().split(sep)[1:]. Er det ingen header, skal readline() ikke stå der; ellers mister du den første dataraden i stillhet.
Semikolon gir .split(';'), komma gir .split(','), mellomrom eller TAB gir bar .split(), faste kolonner gir line[a:b]. Separatoren leses ut av fila slik den er gjengitt i oppgaveteksten. Feil valg gir IndexError med én gang, fordi felt[1] da ikke finnes.
Konverter tallfeltene med int eller float med én gang, inne i innlesingsløkka, og la tekstfeltene være tekst. Da inneholder strukturen ferdige verdier. Glemt konvertering gir ingen feilmelding, bare gale resultater: + på to tallstrenger skjøter dem sammen.
with open(filnavn, 'w') as outfile:, header én gang før løkka, deretter én write per linje der linja er bygget som f-streng og slutter på '\n'. write legger aldri til linjeskift, og godtar bare strenger. Skriver du header, må leseren senere hoppe over den.ny = {} → løkke over .items() → if-betingelse → innsetting → return ny utenfor løkka. I en nøstet struktur står if ytre not in ny: etter betingelsen, slik at ytre nøkler uten treff ikke havner i resultatet med tom indre ordbok. Skriv aldri ny = data.Ett tall per ytre nøkkel, regnet ut fra det indre nivået: total = 0 øverst i den ytre løkka, total += … i den indre, og ny[ytre] = total / len(indre) etter den indre løkka. To feil koster poeng: å nullstille total utenfor den ytre løkka (verdier lekker mellom nøklene), og å dele på len(data) i stedet for len(indre).
Å finne største eller minste verdi uten max: en variabel som holder den beste kandidaten, oppdatert ved hver sammenligning. beste = '' betyr «ingen kandidat ennå» og gjør at den første oppføringen alltid vinner første runde. Nullstillingen hører øverst i den ytre løkka når resultatet skal beregnes per nøkkel.
Når flere rader skal samles under samme nøkkel som en liste: if nøkkel not in data: data[nøkkel] = [] og deretter data[nøkkel].append(verdi). Tilordning i stedet for .append overskriver, så bare den siste raden overlever. Mønsteret gjenkjennes på oppgaveformuleringen «alle X som hører til Y».
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.