8.1 Fillesing til nøstet dictionary
Lese en tekstfil linje for linje og bygge en nøstet dictionary — den 100 %-frekvente L-sjangeren, verdt 8–12 p.
Dette er ett av de fem temaene som går igjen i hvert eneste nyere eksamenssett. Oppgaven der du skal lese en tekstfil og bygge en ordbok av innholdet, kalles i denne boka sjanger L — fillesing til nøstet ordbok, altså «les fila linje for linje og bygg datastrukturen». Den forekommer i 8 av 8 gjennomgåtte avsluttende sett (100 %) og teller 8–12 poeng, ofte delt i tre: (a) les fila til en ordbok, (b) skriv en ordbok til fil, (c) filtrer og returner en ny ordbok. Del (a) er dette kapitlet; (b) og (c) kommer i kap. 8.2.
Prioriteten er den høyeste boka har: dette må sitte. Grunnen er at oppgaven er nesten identisk fra år til år — bare dataene bytter innpakning (målestasjoner, folkeregister, landstatistikk, karakterlister). Har du mønsteret i fingrene, er dette de mest forutsigbare poengene på hele eksamen.
Oppgavene besvares som fritekst — du skriver koden rett inn i tekstfeltet i Inspera, UiOs digitale eksamenssystem, uten hjelpemidler og uten å kunne kjøre den. Derfor øver vi mønsteret til det er automatikk. Og husk delpoeng: sensor gir uttelling for riktig overordnet struktur (riktig with open, riktig løkke, riktig oppbygging av ordboka) selv om du bommer på en separator eller glemmer en .strip().
Forkunnskaper — sist du var her
Dette kapitlet bygger på tre ting fra kap. 1.5 (ordbøker) og kap. 1.4 (strenger). Her er de oppfrisket, med kjørt utskrift — les dem før du går videre.
1. Ordboka: oppslag på nøkkel, ikke på posisjon. En ordbok (på engelsk dictionary) knytter en nøkkel til en verdi. Du slår opp med nøkkelen i hakeparentes.
d = {'Kvitheia': 1180, 'Sandvollen': 760}
d['Nordvika'] = 980
print(d['Kvitheia'], len(d), 'Ospedalen' in d)
for navn in d:
print(navn, d[navn])Utskrift:
1180 3 False
Kvitheia 1180
Sandvollen 760
Nordvika 980Merk at for navn in d gir nøklene, i den rekkefølgen de ble satt inn.
2. Den nøstede ordboka: en ordbok som verdi. Dette er strukturen hele kapitlet handler om. Den indre ordboka må finnes før du kan sette noe inn i den.
d = {}
d['Kvitheia'] = {}
d['Kvitheia']['temp'] = 7.9
d['Kvitheia']['nedbor'] = 1180
print(d)
print(d['Kvitheia']['nedbor'])Utskrift:
{'Kvitheia': {'temp': 7.9, 'nedbor': 1180}}
11803. Å dele opp en tekstlinje. .strip() fjerner blanktegn i begge ender, .split(';') deler strengen der det står semikolon og gir en liste av tekststykker.
linje = 'Ospedalen;8.6;1420\n'
print(linje.strip().split(';'))
print(len(linje.split(';')))Utskrift:
['Ospedalen', '8.6', '1420']
3Trenger du mer om løkker og lister, står de i kap. 1.2 og kap. 1.3. Feilmeldingene som dukker opp når en innlesing går galt, er katalogisert i kap. 2.2.
Løkke 1 — Fra fil til tekstlinjer (~15 min)
En værstasjon lagrer én måling per linje i en tekstfil. Ingen tabell, ingen database — bare tekst. Skal programmet ditt regne på tallene, må du først få tak i linjene, deretter klippe hver linje i felt, og til slutt gjøre tekst om til tall. Vi tar de tre stegene i tur og orden.
Gjennom hele kapitlet bruker vi denne lille datafila. Den kan du lage selv i to sekunder: opprett en tekstfil som heter stasjoner.txt i samme mappe som programmet ditt, og lim inn de fire linjene. Dette er dataene alle eksemplene under leser.
Innholdet i stasjoner.txt:
Kvitheia;7.9;1180
Sandvollen;5.4;760
Ospedalen;8.6;1420
Nordvika;3.1;980Feltene er navn;årsmiddeltemperatur;årsnedbør, med semikolon mellom. Tallene er konstruert for eksempelet — de er ikke hentet fra en virkelig måleserie.
Åpner fila filnavn for lesing og gir deg et filobjekt — en variabel som representerer den åpne fila — under navnet infile. Alt du vil gjøre med fila, står innrykket inne i with-blokken. Når blokken er ferdig, lukkes fila automatisk, også hvis programmet krasjer underveis. Det er hele grunnen til at man bruker with: du kan ikke glemme å lukke.
Finnes ikke fila, stopper programmet med FileNotFoundError. Navnet infile er ikke magisk — det er bare et variabelnavn, men det er navnet sensor forventer å se.
with open('finnes_ikke.txt') as infile:
for line in infile:
print(line)Siste linje i feilmeldingen er:
Utskrift:
FileNotFoundError: [Errno 2] No such file or directory: 'finnes_ikke.txt'Å iterere over filobjektet betyr å gå gjennom fila én linje av gangen. Hver runde i løkka setter line til neste linje i fila — som en streng, med linjeskiftet på slutten. Det siste er den viktigste detaljen i hele kapitlet: linja Kvitheia;7.9;1180 kommer inn som 'Kvitheia;7.9;1180\n'.
Løkka leser bare én linje i minnet av gangen, så mønsteret virker like godt på en fil med fire linjer som på en med fire millioner. Du kan ikke gå tilbake: når løkka er ferdig, er fila lest til ende.
with open('stasjoner.txt') as infile:
n = 0
for line in infile:
n += 1
print(n)Utskrift:
4Skriv ut hver linje i stasjoner.txt med en hakeparentes foran og bak, slik at du ser nøyaktig hvor linja slutter.
Ved å ramme inn strengen med [ og ] blir linjeskiftet synlig: den avsluttende hakeparentesen havner på neste linje.
with open('stasjoner.txt') as infile:
for line in infile:
print('[' + line + ']')Utskrift:
[Kvitheia;7.9;1180
]
[Sandvollen;5.4;760
]
[Ospedalen;8.6;1420
]
[Nordvika;3.1;980
]Den hengende ] er linjeskiftet inne i line. Legg til .strip(), og innrammingen strammer seg:
with open('stasjoner.txt') as infile:
for line in infile:
print('[' + line.strip() + ']')Utskrift:
[Kvitheia;7.9;1180]
[Sandvollen;5.4;760]
[Ospedalen;8.6;1420]
[Nordvika;3.1;980](Midtveisnivå, sjanger A — kodesporing, altså «hva skriver dette programmet ut?».) Programmet leser stasjoner.txt slik den står gjengitt over. Hva skriver det ut? Skriv utskriften nøyaktig, med mellomrom slik terminalen viser den.
with open('stasjoner.txt') as infile:
n = 0
for line in infile:
n += 1
felt = line.split(';')
print(n, len(felt), felt[0])Gir en ny streng der alle blanktegn — mellomrom, TAB og linjeskift — er fjernet fra begge ender. Midt i strengen røres ingenting. Originalen endres ikke; .strip() returnerer en kopi, så du må bruke returverdien.
Ved fillesing er dette rutinehandlingen på hver linje: line.strip() kvitter deg med linjeskiftet før du deler opp.
linje = ' Kvitheia \n'
print('[' + linje.strip() + ']')
print('[' + linje + ']')Utskrift:
[Kvitheia]
[ Kvitheia
]Deler en streng i biter der separator står, og gir en liste av bitene. Separatoren selv kastes. 'a;b;c'.split(';') gir ['a', 'b', 'c'] — tre biter fra to semikolon.
Uten argument, altså bar .split(), deles det på vilkårlige blanktegn (én eller flere mellomrom, TAB, linjeskift), og tomme biter droppes. Det er en helt annen oppførsel, og forveksling mellom de to er en av de vanligste feilene i sjangeren: er fila semikolonseparert, gir bar .split() deg én eneste bit.
linje = 'Ingrid Hauge;5003;1998\n'
print(linje.strip().split(';'))
print(linje.split())Utskrift:
['Ingrid Hauge', '5003', '1998']
['Ingrid', 'Hauge;5003;1998']Gjør en tallstreng om til et tall. Alt som kommer ut av en fil er tekst, også når det ser ut som et tall, så konverteringen må gjøres eksplisitt. int('1180') gir heltallet 1180; float('8.6') gir flyttallet 8.6.
Begge tåler blanktegn rundt tallet — også et linjeskift. int('1180\n') er altså 1180, ikke en feil. Det er verdt å merke seg, for det betyr at en glemt .strip() ikke gir feilmelding når siste felt er et tall; den slår først til når siste felt er tekst. Er strengen ikke et gyldig tall, får du ValueError.
print(int('1180\n'), float(' 8.6 '))Utskrift:
1180 8.6Del linja 'Ospedalen;8.6;1420\n' i felt, og vis forskjellen mellom å legge sammen feltene som tekst og som tall.
Feltene er strenger. + på strenger skjøter dem sammen; + på tall legger dem sammen. Konverteringen avgjør altså hva regnestykket betyr.
line = 'Ospedalen;8.6;1420\n'
felt = line.strip().split(';')
print(felt)
print(felt[1] + felt[2])
print(float(felt[1]) + int(felt[2]))Utskrift:
['Ospedalen', '8.6', '1420']
8.61420
1428.6Midterste linje er ren tekstskjøting: '8.6' og '1420' blir '8.61420'. Ingen feilmelding, bare et meningsløst resultat — derfor er dette en klassisk felle på flervalg. Nederste linje er det du var ute etter.
.split(). Bruker du bar .split() på en semikolonseparert fil, får du én bit per linje. Neste linje i programmet, felt[1], gir da IndexError: list index out of range. Se på fila før du velger separator: er feltene skilt med semikolon, skriv .split(';'); med komma, .split(','); med mellomrom eller TAB, bar .split().Feil #7 — glemt .strip(). Siste felt på hver linje drar linjeskiftet med seg. Er det feltet et tall, merker du det ikke, fordi int og float tåler blanktegn. Er det tekst, får du en verdi som ser riktig ut, men ikke er det:
data = {}
with open('maalinger.txt') as infile:
for line in infile:
felt = line.split(';')
data[felt[0] + ' ' + felt[1]] = felt[3]
for nokkel in data:
print('[' + nokkel + ']', '[' + data[nokkel] + ']', data[nokkel] == 'regn')Utskrift:
[Kvitheia 2024-03-01] [regn
] False
[Kvitheia 2024-03-02] [snø
] False
[Ospedalen 2024-03-01] [regn
] FalseSammenligningen data[nokkel] == 'regn' er False for alle radene, selv den som opplagt er regn — for verdien er 'regn\n', ikke 'regn'. Fila eksempelet bruker, er maalinger.txt under; den møter du igjen senere i kapitlet.
Innholdet i maalinger.txt:
Kvitheia;2024-03-01;4.2;regn
Kvitheia;2024-03-02;-1.8;snø
Ospedalen;2024-03-01;6.5;regnVanen som fjerner begge feilene: skriv alltid line.strip().split(separator) i én bevegelse.
(Midtveisnivå, sjanger A — kodesporing.) Hva skriver dette programmet ut?
linje = 'Nordvika;3.1;980\n'
print(linje.split(';')[0], linje.split()[0])
print(len(linje.split(';')[2]), len(linje.strip().split(';')[2]))Løkke 2 — Bygg den nøstede ordboka (~15 min)
Nå har du linjene og feltene. Spørsmålet er hvor du skal legge dem. Du kunne samlet alt i en liste av lister, [['Kvitheia', 7.9, 1180], ...], men da må du lete deg gjennom lista hver gang du vil finne Kvitheia. Skal programmet svare på «hvor mye nedbør fikk Ospedalen?», er ordboka riktig verktøy: stasjonsnavnet er nøkkelen.
Og fordi hver stasjon har flere tall, blir verdien selv en ordbok. Det er den nøstede ordboka: ytre nøkkel er det som identifiserer raden (stasjon, ID, land, kandidatnummer), indre nøkler er feltnavnene.
data['Ospedalen']['nedbor']
ytre indreDette er det kanoniske mønsteret i sjanger L. Fem steg, alltid i denne rekkefølgen:
1. Lag den tomme ytre ordboka: data = {}.
2. Åpne fila: with open(filnavn) as infile:.
3. Hopp over en eventuell header-linje: infile.readline().
4. For hver linje: felt = line.strip().split(separator) — strip før split.
5. Sett inn i strukturen med typekonvertering, og returner data etter at with-blokken er ferdig.
I kode:
def les_fil(filnavn):
data = {}
with open(filnavn) as infile:
for line in infile:
felt = line.strip().split(';')
data[felt[0]] = {'temp': float(felt[1]), 'nedbor': int(felt[2])}
return data
print(len(les_fil('stasjoner.txt')))Utskrift:
4Legg merke til at return data står utenfor with-blokken men inne i funksjonen. Står return inne i løkka, returnerer du etter første linje — en fast delpoengfelle. Sensor gir uttelling for disse fem stegene hver for seg, så skriv dem alltid ned i rekkefølge, også om du er usikker på detaljene i steg 5.
En nøstet ordbok må bygges utenfra og inn. data[ytre][indre] = verdi virker bare hvis data[ytre] allerede er en ordbok. Er den ikke opprettet, får du KeyError — Python leter etter nøkkelen ytre for å sette noe inn i den, og finner den ikke.
Standardgrepet er derfor to linjer: sjekk om den ytre nøkkelen finnes, opprett en tom ordbok hvis ikke, og sett først deretter inn.
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}}'Vest' opprettes bare første gang; andre gang finnes nøkkelen, og telleren økes i den ordboka som alt ligger der.
Har hver rad i fila sin egen ytre nøkkel, kan du hoppe over sjekken og skrive hele den indre ordboka i én tilordning: data[felt[0]] = {'temp': ..., 'nedbor': ...}. Trenger flere rader å samles under samme ytre nøkkel, må du bruke sjekken.
Fila stasjoner.txt har linjer på formen navn;middeltemperatur;nedbør. Skriv les_stasjoner(filnavn) som returnerer en ordbok fra stasjonsnavn til en indre ordbok {'temp': ..., 'nedbor': ...} med tallene konvertert. Vis at oppslag fungerer.
Her har hver linje sin egen stasjon, så hele den indre ordboka kan settes inn i én tilordning. Utpakkingen navn, temp, nedbor = ... fungerer fordi vi vet at hver linje har nøyaktig tre felt.
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
d = les_stasjoner('stasjoner.txt')
print(d['Ospedalen'])
print(d['Ospedalen']['nedbor'])
print(len(d))Utskrift:
{'temp': 8.6, 'nedbor': 1420}
1420
4Tre ting å legge merke til i utskriften. d['Ospedalen'] gir hele den indre ordboka, mens d['Ospedalen']['nedbor'] gir tallet — forskjellen mellom struktur og element. Og len(d) er 4, altså antall stasjoner, ikke antall tall.
Dette er svaret sensor er ute etter på del (a) av en sjanger-L-oppgave. Fire linjer kode, full uttelling.
Fila byer.txt er kommaseparert og har ingen header-linje:
Halsvik,4820,31.4
Storheim,15630,88.2
Varden,9075,52.7Feltene er navn,folketall,areal (areal i kvadratkilometer). Skriv les_byer(filnavn) som returnerer en nøstet ordbok fra bynavn til {'folketall': ..., 'areal': ...}, med folketall som heltall og areal som flyttall. Vis at les_byer('byer.txt')['Varden'] og folketallet til Storheim kommer riktig ut.
Bygger en ordbok av to lister: første liste blir nøkler, andre blir verdier, paret posisjon for posisjon. Det er en snarvei når feltnavnene er kjent på forhånd, eller når de kommer fra en header-linje.
zip stopper ved den korteste av de to listene — den fyller ikke ut. Er det tre feltnavn og to verdier, får du en ordbok med to par, uten noen advarsel.
print(dict(zip(['temp', 'nedbor'], [7.9, 1180])))
print(dict(zip(['temp', 'nedbor', 'vind'], [7.9, 1180])))Utskrift:
{'temp': 7.9, 'nedbor': 1180}
{'temp': 7.9, 'nedbor': 1180}De to linjene gir samme ordbok, og det er nettopp poenget: 'vind' forsvant lydløst.
Skriv les_stasjoner om slik at feltnavnene står i en liste og den indre ordboka bygges med dict(zip(...)).
Fordelen er at feltnavnene står på ett sted. Skal fila få en kolonne mer, endrer du to lister, ikke innsettingslinja.
def les_stasjoner(filnavn):
feltnavn = ['temp', 'nedbor']
data = {}
with open(filnavn) as infile:
for line in infile:
felt = line.strip().split(';')
data[felt[0]] = dict(zip(feltnavn, [float(felt[1]), int(felt[2])]))
return data
print(les_stasjoner('stasjoner.txt')['Nordvika'])Utskrift:
{'temp': 3.1, 'nedbor': 980}Begge variantene er fullgode på eksamen. Velg den eksplisitte fra Eksempel 3 når feltene er få og har ulik type, og dict(zip(...)) når feltene er mange eller kommer fra en header.
(Midtveisnivå, sjanger A — kodesporing.) Fila stasjoner_dobbel.txt inneholder:
Kvitheia;7.9;1180
Ospedalen;8.6;1420
Kvitheia;8.4;1205Hva skriver dette programmet ut?
data = {}
with open('stasjoner_dobbel.txt') as infile:
for line in infile:
navn, temp, nedbor = line.strip().split(';')
data[navn] = {'temp': float(temp), 'nedbor': int(nedbor)}
print(len(data))
print(data['Kvitheia'])Leser én linje fra fila og flytter leseposisjonen fram til neste. Kalt før løkka er dette standardgrepet for å hoppe over en header-linje, altså den første linja som inneholder kolonnenavn i stedet for data.
Returverdien er linja som streng, med linjeskift. Vil du bruke kolonnenavnene som feltnavn, tar du vare på den; vil du bare hoppe over den, kaller du readline() uten å bruke resultatet til noe.
with open('nedbor.txt') as infile:
forste = infile.readline()
n = 0
for line in infile:
n += 1
print(forste.strip())
print(n)Utskrift:
stasjon;jan;feb;mar
3Fila har fire linjer, men løkka ser bare tre: readline() har alt spist den første.
Fila nedbor.txt har en header-linje og deretter én linje per stasjon med nedbør for tre måneder. Les den til en nøstet ordbok der de indre nøklene hentes fra headeren.
Innholdet i nedbor.txt:
stasjon;jan;feb;mar
Kvitheia;104;88;96
Sandvollen;61;47;58
Ospedalen;131;112;120Headeren leses med readline(), deles opp, og første felt ('stasjon') kuttes bort med slicing [1:] — det er en nøkkel i den ytre ordboka, ikke et feltnavn i den indre.
with open('nedbor.txt') as infile:
feltnavn = infile.readline().strip().split(';')[1:]
data = {}
for line in infile:
felt = line.strip().split(';')
data[felt[0]] = dict(zip(feltnavn, [int(v) for v in felt[1:]]))
print(feltnavn)
print(data['Sandvollen'])
print(data['Sandvollen']['feb'])Utskrift:
['jan', 'feb', 'mar']
{'jan': 61, 'feb': 47, 'mar': 58}
47[int(v) for v in felt[1:]] konverterer alle tallfeltene i én bevegelse — nyttig når antallet kolonner ikke er kjent på forhånd. Løsningen virker like godt om fila får en kolonne for april: både feltnavn og verdilista vokser av seg selv.
Glemmer du readline(), blir headeren behandlet som data. Da forsøker programmet å konvertere kolonnenavnet til et tall:
with open('nedbor.txt') as infile:
for line in infile:
felt = line.strip().split(';')
print(int(felt[1]))Siste linje i feilmeldingen er:
Utskrift:
ValueError: invalid literal for int() with base 10: 'jan'Dette er feil #7 i den andre utgaven: ikke glemt .strip(), men glemt readline(). Feilmeldingen er lett å kjenne igjen på eksamen — ser du ValueError med et kolonnenavn i teksten, er det headeren som er på vei gjennom konverteringen.
Og motsatt: kaller du readline() på en fil uten header, mister du den første dataraden i stillhet. Derfor er første grep i enhver sjanger-L-oppgave å lese oppgavetekstens gjengivelse av fila: står det en kolonnenavn-linje øverst eller ikke?
Bruk nedbor.txt fra Eksempel 5.
a) Skriv les_nedbor(filnavn) som returnerer en nøstet ordbok fra stasjonsnavn til {'jan': ..., 'feb': ..., 'mar': ...} med heltall som verdier, der de indre nøklene hentes fra header-linja.
b) Bruk funksjonen til å skrive ut den indre ordboka for Ospedalen og summen av de tre månedene.
Løkke 3 — Hvorfor ordbok, og ikke liste? (~10 min)
— naturlig pausepunkt —
Oppgaveteksten sier nesten alltid «returner en ordbok». Det er ikke en tilfeldig preferanse, og du bør kunne begrunne den, for begrunnelsen dukker opp som flervalgsspørsmål.
Poenget er hvordan du finner igjen en rad. I en liste av lister må du gå gjennom radene til du treffer navnet du leter etter. I en ordbok går oppslaget rett på nøkkelen. Under regner vi på forskjellen i stedet for å påstå den.
Sjekker om en nøkkel finnes i ordboka, og gir True eller False. På en ordbok ser testen på nøklene, ikke på verdiene. Den er billig: Python går rett på nøkkelen, uansett hvor stor ordboka er.
Testen har to bruksområder i dette kapitlet: å opprette den indre ordboka bare første gang (if navn not in data:), og å unngå KeyError når du er usikker på om nøkkelen finnes.
d = {'Kvitheia': 1180}
print('Kvitheia' in d, 'Fornebu' in d)
print(1180 in d)Utskrift:
True False
FalseSiste linje er poenget: 1180 er en verdi, ikke en nøkkel, så testen er False.
Samme fire stasjoner lagret på to måter: som en liste av lister og som en ordbok. Tell hvor mange rader et navneoppslag må gå gjennom i hvert tilfelle.
Vi lar søkefunksjonen returnere både verdien og antall rader den måtte besøke, slik at forskjellen blir et tall og ikke en påstand.
def finn_i_liste(tabell, navn):
besokt = 0
for rad in tabell:
besokt += 1
if rad[0] == navn:
return rad[1], besokt
return None, besokt
tabell = [['Kvitheia', 7.9], ['Sandvollen', 5.4], ['Ospedalen', 8.6], ['Nordvika', 3.1]]
ordbok = {'Kvitheia': 7.9, 'Sandvollen': 5.4, 'Ospedalen': 8.6, 'Nordvika': 3.1}
for navn in ['Kvitheia', 'Nordvika']:
verdi, besokt = finn_i_liste(tabell, navn)
print(f'liste : {navn:11s} gir {verdi}, etter {besokt} rader')
print(f'ordbok: {navn:11s} gir {ordbok[navn]}, ett oppslag')Utskrift:
liste : Kvitheia gir 7.9, etter 1 rader
ordbok: Kvitheia gir 7.9, ett oppslag
liste : Nordvika gir 3.1, etter 4 rader
ordbok: Nordvika gir 3.1, ett oppslagLigger raden først, koster lista én sammenligning. Ligger den sist, koster den fire. Med 4 000 stasjoner koster den i verste fall 4 000 — for hvert oppslag. Ordboka koster ett oppslag uansett, fordi Python regner ut hvor nøkkelen hører hjemme i stedet for å lete.
Legg dessuten merke til hva koden ser ut som. Lista trenger en hjelpefunksjon med løkke, if og to returverdier. Ordboka trenger ordbok[navn]. Den lesbarheten er halve begrunnelsen for at oppgaveteksten ber om en ordbok — og den er grunnen til at fasiten på sjanger L nesten er kortere enn oppgaveteksten.
En kollega har allerede lest fila inn i en liste av lister:
tabell = [['Kvitheia', 7.9, 1180], ['Sandvollen', 5.4, 760],
['Ospedalen', 8.6, 1420], ['Nordvika', 3.1, 980]]a) Skriv koden som gjør tabell om til en nøstet ordbok fra navn til {'temp': ..., 'nedbor': ...}.
b) Skriv ut den indre ordboka for Sandvollen, og resultatet av å teste om 'Fornebu' og 'Nordvika' finnes som nøkler.
I dette kapitlet og i kap. 1.5 betyr d konsekvent ordboka (dictionary). I de numeriske delene av boka betyr enkeltbokstavparametere noe helt annet: d kan være en modellparameter, og dt er et tidssteg. Bokstaven bærer altså ingen mening i seg selv — den får den fra sammenhengen.
Rådet er å skrive data når det er en datastruktur du bygger, og d bare når koden er kort nok til å ses i sin helhet. Sensor leser koden din som helhet, så gode navn er billige poeng når resten er usikkert.
To navn du aldri skal bruke som variabler: dict og list. Skriver du dict = {}, har du ødelagt dict(zip(...)) for resten av programmet, og feilmeldingen som kommer, sier ingenting om hvorfor. Samme gjelder sum, len, str, int, float, max, min og type — se listen i kap. 1.1.
Løkke 4 — Bruk strukturen (~15 min)
Del (a) av en eksamensoppgave slutter med return data. Del (b) og (c) spør alltid om noe du skal gjøre med strukturen: finn den varmeste stasjonen, regn ut et gjennomsnitt, skriv en tabell. Da må du iterere over ordboka.
Gir deg nøkkel og verdi samtidig, ett par per runde i løkka, slik at du kan skrive for navn, verdier in data.items(): i stedet for for navn in data: etterfulgt av data[navn]. Rekkefølgen er innsettingsrekkefølgen.
I en nøstet ordbok er verdier selv en ordbok, så du fortsetter med verdier['temp']. To nivåer krever to løkker: en over data.items() og en over den indre.
d = {'Kvitheia': {'x': 1}, 'Nordvika': {'x': 4}}
for navn, verdier in d.items():
print(navn, verdier['x'], end=' ')Utskrift:
Kvitheia 1 Nordvika 4 Merk end=' ': alt havner på én linje, og det står ett mellomrom helt til slutt — også etter det siste tallet, der utskriften bare stopper uten linjeskift.
Les stasjoner.txt til en nøstet ordbok, skriv en tabell med navn, temperatur og nedbør i pene kolonner, og skriv ut navnet på den varmeste stasjonen. Dette er et helt sjanger-L-svar av den typen som gir 8–12 poeng.
Vi gjenbruker les_stasjoner fra Eksempel 3 uendret og legger til to ting: en hjelpefunksjon som finner største verdi, og en utskriftsløkke med f-strenger for kolonnebredde.
Hjelpefunksjonen bruker det vanlige «beste så langt»-mønsteret. Startverdien er en tom streng, som betyr «ingen kandidat ennå», slik at første stasjon alltid blir første kandidat:
def varmeste(data):
beste = ''
for navn in data:
if beste == '' or data[navn]['temp'] > data[beste]['temp']:
beste = navn
return besteDeretter tabellen. f'{navn:11s}' gir navnet en fast bredde på 11 tegn, f'{...:6.1f}' gir tallet bredde 6 med én desimal, og f'{...:7d}' gir heltallet bredde 7 — det er slik man får kolonner som står under hverandre:
data = les_stasjoner('stasjoner.txt')
for navn in data:
print(f'{navn:11s}{data[navn]["temp"]:6.1f}{data[navn]["nedbor"]:7d}')
print('Varmest:', varmeste(data))Utskrift:
Kvitheia 7.9 1180
Sandvollen 5.4 760
Ospedalen 8.6 1420
Nordvika 3.1 980
Varmest: OspedalenSlik ville sensor lese dette: innlesingen er del (a) og bærer mesteparten av poengene. Utskriftsløkka gir uttelling selv med feil kolonnebredder — det er f-strengens struktur som vurderes, ikke om tabellen er pen. varmeste gir uttelling for riktig sammenligningsmønster; hadde du brukt >= i stedet for >, hadde svaret vært det samme her og fortsatt vært riktig.
Bruk maalinger.txt fra advarselen tidligere i kapitlet. Feltene er stasjon;dato;temperatur;værtype, og samme stasjon har flere linjer.
Innholdet i maalinger.txt:
Kvitheia;2024-03-01;4.2;regn
Kvitheia;2024-03-02;-1.8;snø
Ospedalen;2024-03-01;6.5;regna) Skriv les_maalinger(filnavn) som returnerer en ordbok med stasjonsnavn som ytre nøkkel, dato som neste nøkkel, og {'temp': ..., 'vaer': ...} innerst.
b) Skriv snitt(data, stasjon) som returnerer gjennomsnittstemperaturen for én stasjon.
c) Vis den indre ordboka for Kvitheia, og gjennomsnittet for Kvitheia og Ospedalen med to desimaler.
En medstudents program skal lese maalinger.txt (samme fil som i Oppgave 7) til en ordbok med stasjon og dato som nøkler og temperaturen som verdi. Det krasjer.
data = {}
with open('maalinger.txt') as infile:
for line in infile:
felt = line.split()
data[felt[0]][felt[1]] = float(felt[2])
print(data)a) Hvilken feilmelding får programmet, og hvorfor?
b) Hvilke to feil må rettes? Skriv den rettede koden.
Begrepsbank
Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.
En fil som inneholder tegn organisert i linjer, atskilt av linjeskift. Alt i en tekstfil er tekst — også det som ser ut som tall. Derfor må hvert tallfelt konverteres med int eller float etter innlesing. IN1900 leser bare rene tekstfiler; andre filformater er utenfor pensum.
Variabelen with open(...) as infile: gir deg. Den representerer den åpne fila og holder styr på hvor langt du har lest. Fordi leseposisjonen bare går framover, kan du ikke iterere over samme filobjekt to ganger — andre gang er fila tom. Trenger du to gjennomlesinger, åpner du fila på nytt.
Tegnet som skiller feltene på en linje: semikolon, komma, TAB eller mellomrom. Separatoren bestemmer argumentet til .split(). Det finnes ingen måte å gjette den — du leser den ut av fila (eller av oppgavetekstens gjengivelse av fila). En linje med tre semikolon har fire felt.
Den første linja i en fil, når den inneholder kolonnenavn i stedet for data. Den hoppes over med infile.readline() før løkka, eller brukes som kilde til feltnavnene i den indre ordboka. Glemmer du den, ender kolonnenavnet inne i en int() og gir ValueError.
En ordbok der verdiene selv er ordbøker: data[ytre][indre]. Ved fillesing er den ytre nøkkelen det som identifiserer raden (stasjon, ID, land, kandidat), og de indre nøklene er feltnavnene. Dette er datastrukturen sjanger L nesten alltid ber om, og den kan ha tre nivåer når samme rad-ID har flere målinger.
Ytre nøkkel er den du slår opp først (data['Ospedalen']) og gir deg en struktur. Indre nøkkel slår opp i den strukturen (data['Ospedalen']['nedbor']) og gir deg et element. Å forveksle de to — å tro at data['Ospedalen'] gir et tall — er den vanligste sporingsfeilen i sjangeren.
Å gjøre tekstfeltene om til tall der de skal regnes med, og la dem være tekst der de ikke skal. Regelen er å konvertere med én gang, inne i innlesingsløkka, slik at strukturen inneholder ferdige tall. felt[1] + felt[2] på tekst skjøter strengene sammen uten å klage, så en glemt konvertering gir feil svar, ikke feilmelding.
Et oppslag i en ordbok går rett på nøkkelen og koster like lite uansett hvor stor ordboka er. Et søk i en liste går gjennom radene til det treffer, og koster i verste fall like mye som listas lengde. Det er hovedgrunnen til at fillesingsoppgaver ber om en ordbok når dataene skal slås opp på navn eller ID.
En ordbok kan ikke ha samme nøkkel to ganger. Leser du to linjer med samme stasjonsnavn og setter inn med data[navn] = ..., overskriver den andre den første — ordboka får én oppføring, med den siste linjas verdier. Skal begge målingene beholdes, må neste nøkkelnivå (typisk dato) skille dem.
Feilen du får når du slår opp en nøkkel som ikke finnes: data['Fornebu'] når 'Fornebu' ikke er satt inn, eller data[ytre][indre] = verdi når den indre ordboka ikke er opprettet. Den forebygges med if nøkkel not in data:. Feiltypene er katalogisert i kap. 2.2.
for k in data gir nøklene, én per runde, i innsettingsrekkefølge. for k, v in data.items() gir nøkkel og verdi samtidig. I en nøstet ordbok trenger du to løkker for å komme til de innerste verdiene: én over den ytre ordboka og én over hver indre.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.