4.3 Unntakshåndtering rundt fil-I/O
try/except rundt filoperasjoner: håndtere «fil finnes ikke», gi en feilmelding og returnere None i stedet for å krasje.
- try/except rundt filoperasjoner står i ~60 % av settene (6 av 10 sittinger), oftest som et krav i deloppgaven som leser fila: «skriv en feilmelding og returner None hvis fila ikke finnes, i stedet for at programmet krasjer».
- Sjangeren er E — filinnlesing og I — lagring, med robusthet som tillegg. Robust fil-I/O er et toppscore-signal: sensor ser etter det, og det er billig å levere.
- Prioritet: må kunne — bokas midterste nivå, av «må sitte», «må kunne» og «bør kjenne til».
Feilene som koster poeng her er ikke feil i koden din — de er feil i håndteringen:
- #6 — glemme å returnere noe fornuftig i feilgrenen, så kalleren krasjer litt senere i stedet.
- Fange feil type, så unntaket slipper forbi likevel.
- Legge try for bredt, slik at ekte programmeringsfeil svelges stille og du aldri får vite om dem.
Kapitlet tar ~45 minutter i fire løkker. Du trenger ikke kunne alle feiltypene utenat — du trenger å kjenne igjen de fire som oppstår rundt filer og data.
- kap. 4.1 — filinnlesing: open, for linje in f:, .strip().split() og konvertering. Det er nettopp disse fire stegene som kan gå galt.
- kap. 4.2 — filskriving, som har sine egne feilkilder.
- kap. 2.1 — funksjoner og return, siden hele poenget er hva funksjonen skal returnere når det går galt.
- kap. 1.2 — betingelser, som er alternativet til try/except når feilen kan forutses.
Løkke 1 — Hva skjer når det går galt (~10 min)
Du har skrevet en perfekt filleser. Så skriver brukeren filnavnet feil, eller fila er ikke lastet ned ennå, eller den ligger i en annen mappe. Programmet ditt stopper midt i, med en feilmelding på seks linjer som brukeren ikke skjønner noe av.
Det er ikke en feil i koden din. Det er en situasjon koden din ikke har tatt stilling til — og det er nøyaktig det unntakshåndtering handler om.
Et unntak er en feilsituasjon som oppstår mens programmet kjører, og som stopper det der og da hvis ingen tar imot den.
Det engelske ordet er exception, og du ser det i feilmeldingene. Boka bruker «unntak» i prosa og de engelske typenavnene i kode, siden det er dem Python skriver ut.
Merk skillet mot en syntaksfeil: den oppdages før programmet i det hele tatt starter, og kan ikke fanges. Et unntak oppstår først under kjøring, og kan fanges.
Utskriften Python gir når et unntak ikke blir fanget. Den leses nedenfra og opp: siste linje sier hva som gikk galt, linjene over sier hvor.
Siste linje har alltid formen FeilType: forklaring. Det er den ene linja du trenger — typen står først, og den er navnet du skriver i except.
Å lese den siste linja er en eksamensferdighet i seg selv: den forteller deg både hvilken feiltype du skal fange, og hva som utløste den.
Hva skjer når et program prøver å lese en fil som ikke finnes? Kjør innlesingen fra kap. 4.1 mot filnavnet salgstall.txt, som ikke er opprettet.
Koden er helt riktig skrevet. Det er situasjonen som er gal:
# tilsiktet feil: fila finnes ikke, så programmet stopper her
f = open('salgstall.txt', 'r')
tabell = []
for linje in f:
tabell.append(linje.strip().split(';'))
f.close()Kodeblokken er merket som tilsiktet feilende. Programmet stopper på open-linja, og siste linje i feilmeldingen er:
FileNotFoundError: [Errno 2] No such file or directory: 'salgstall.txt'Les den linja nøye. FileNotFoundError er typen, og resten er forklaringen med filnavnet i. Ingenting av det som står etter open ble utført: tabell ble aldri opprettet, og close ble aldri kjørt.
Dette er problemet i et nøtteskall. Programmet ditt er ikke feil — det mangler bare en plan for hva som skal skje når fila ikke er der.
Grunnformen. Koden som kan gå galt, legges i try-blokken. Går det galt, hopper Python rett til den except-grenen som passer til feiltypen, og fortsetter der.
try:
f = open('salgstall.txt', 'r')
print('Fila ble åpnet')
except FileNotFoundError:
print('Fant ikke filen: salgstall.txt')
print('Programmet fortsetter')Utskrift:
Fant ikke filen: salgstall.txt
Programmet fortsetterTre ting å legge merke til: print('Fila ble åpnet') ble aldri kjørt, siden feilen oppsto på linja over. Programmet stoppet ikke. Og den siste linja kjørte som normalt — feilen er håndtert, ikke skjult.
Går alt bra, hoppes hele except-grenen over.
Skriv les_tabell(filnavn) som leser fila til en 2D-tabell, men som skriver «Fant ikke filen: <navn>» og returnerer None i stedet for å stoppe når fila mangler. Vis begge utfallene.
maalinger.txt:Blindern;12.4;61
Kjevik;15.1;54
Værnes;9.8;73def les_tabell(filnavn):
try:
f = open(filnavn, 'r')
except FileNotFoundError:
print('Fant ikke filen:', filnavn)
return None
tabell = []
for linje in f:
tabell.append(linje.strip().split(';'))
f.close()
return tabell
print(les_tabell('maalinger.txt'))
resultat = les_tabell('salgstall.txt')
print(resultat)Utskrift:
[['Blindern', '12.4', '61'], ['Kjevik', '15.1', '54'], ['Værnes', '9.8', '73']]
Fant ikke filen: salgstall.txt
NoneBegge utfallene er med i utskriften. Første kall gir tabellen, andre gir feilmeldingen og deretter None.
return None er ikke pynt. Uten den ville funksjonen falt igjennom og returnert None uansett — men da hadde den også prøvd å lese fra en fil som aldri ble åpnet, og stoppet med NameError i stedet. Feilgrenen må avsluttes.
Merk at bare open ligger i try-blokken. Det er den eneste linja som kan utløse FileNotFoundError. Legger du hele løkka inni også, fanger du feil du ikke mente å fange — mer om det i løkke 4.
Slik ville sensor sett på det: de tre poenggivende punktene er try rundt åpningen, riktig feiltype i except, og at funksjonen returnerer noe kalleren kan sjekke. En løsning som returnerer en tom liste [] i stedet for None, gir like full pott — så lenge oppgaveteksten ikke sier noe annet, og så lenge du er konsekvent.
(Innstegsoppgave — sjanger E, filinnlesing med robusthet.) Se på denne koden:
try:
tall = int('femtifem')
print('Konvertert:', tall)
except ValueError:
print('Ikke et tall')
print('Ferdig')a) Hvilke av de tre print-setningene kjører?
b) Hva ville skjedd uten try/except?
Løkke 2 — De fem feiltypene du møter rundt filer og data (~12 min)
Du trenger ikke kunne alle Pythons feiltyper. Du trenger de fem som oppstår i kjeden les fil, del linja, konverter feltet, slå opp verdien, regn. De kommer i den rekkefølgen, én per steg.
Fila finnes ikke, eller navnet er skrevet feil. Oppstår i open, og bare der.
try:
f = open('salgstall.txt', 'r')
except FileNotFoundError as e:
print('Feilmeldingen sier:', e)Utskrift:
Feilmeldingen sier: [Errno 2] No such file or directory: 'salgstall.txt'Merk at feilen kommer med filnavnet i teksten. Det er verdt å ta med i din egen melding også — «Fant ikke filen: salgstall.txt» hjelper brukeren, «Noe gikk galt» gjør det ikke.
Ved skriving ('w') er denne feilen sjelden: mangler fila, blir den opprettet.
Verdien har riktig type, men gal form. Den vanligste kilden i dette faget er int() eller float() på et felt som ikke er et tall — for eksempel en overskriftsrad som ble med i løkka, eller et ødelagt felt i fila.
for tekst in ['42', 'femtifem', '12.4']:
try:
print(tekst, 'gir', int(tekst))
except ValueError:
print(tekst, 'kan ikke bli et heltall')Utskrift:
42 gir 42
femtifem kan ikke bli et heltall
12.4 kan ikke bli et heltallLegg merke til at '12.4' også feiler: int() godtar ikke desimaltall som tekst. Der må du bruke float().
ValueError kastes også av .index(navn) når navnet ikke finnes i lista (kap. 4.1).
Du ber om en plass som ikke finnes i lista. Rundt filer er kilden nesten alltid en linje med for få felt — en blank linje til slutt, eller en rad som mangler en verdi.
linjer = ['mandag;løpetur;42', 'onsdag;svømming']
for linje in linjer:
felt = linje.strip().split(';')
try:
print(felt[0], felt[2])
except IndexError:
print(felt[0], 'mangler et felt (fikk', len(felt), 'felt)')Utskrift:
mandag 42
onsdag mangler et felt (fikk 2 felt)Feilmeldingen er list index out of range. Motgiften er enten try/except, eller en forutseende test: if len(felt) == 3:.
Er feilen forventet og lett å teste for, er if-varianten ofte den ryddigste.
Du slår opp en nøkkel som ikke finnes i ordboka.
d = {'Bergen': 120, 'Tromsø': 45}
try:
print(d['Stavanger'])
except KeyError:
print('Byen står ikke i ordboka')
print(d.get('Stavanger', 0))
print('Stavanger' in d)Utskrift:
Byen står ikke i ordboka
0
FalseDe to siste linjene viser alternativene som unngår feilen helt: d.get(nøkkel, standardverdi) gir standardverdien i stedet for å kaste feil, og in-testen spør på forhånd.
Regelen boka følger: kan feilen forutses billig, forutse den. Kan den ikke det, fang den.
Deling på null. I filoppgaver oppstår den nesten alltid ett bestemt sted: et gjennomsnitt regnet ut over en tom tabell.
def snitt(tall):
try:
return sum(tall) / len(tall)
except ZeroDivisionError:
return None
print(snitt([12.4, 15.1, 9.8]))
print(snitt([]))Utskrift:
12.433333333333332
NoneHer er if len(tall) == 0: return None like god, og litt tydeligere. Poenget er at tomtilfellet må være håndtert — det er feilkode #6, og et av de fire fullscore-kravene sensor ser etter.
Feil type i en operasjon: å legge sammen tekst og tall, eller å gi write et heltall (kap. 4.2).
try:
print('54' + 54)
except TypeError as e:
print('TypeError:', e)
print('54' + str(54))
print(int('54') + 54)Utskrift:
TypeError: can only concatenate str (not "int") to str
5454
108TypeError er som regel et symptom på feilkode #1 — glemt konvertering. Den fanges sjelden med try/except i dette faget; den fikses ved å konvertere riktig, som de to siste linjene viser.
Gir deg selve feilobjektet i variabelen e, slik at du kan skrive ut Pythons egen forklaring ved siden av din egen.
try:
tall = int('femtifem')
except ValueError as e:
print('Klarte ikke å konvertere feltet.')
print('Python sier:', e)Utskrift:
Klarte ikke å konvertere feltet.
Python sier: invalid literal for int() with base 10: 'femtifem'Navnet e er en konvensjon, ikke en regel. Formen er nyttig når du skriver en feilmelding til en bruker, og under feilsøking — men den er aldri påkrevd.
except: fanger alt. Også skrivefeil i din egen kode:try:
f = open('maalinger.txt', 'r')
antall = 0
for linje in f:
antall = antal + 1
f.close()
except:
print('Kunne ikke lese fila')Utskrift:
Kunne ikke lese filaMeldingen er usann. Fila ble lest helt fint; det som gikk galt, var skrivefeilen antal i stedet for antall, som gir NameError. Den nakne except-grenen svelget den, og du sitter igjen med en feilmelding som peker på helt feil sted.
Arkivets løsningsforslag trekker minimalt for en naken except, men spesifikk er alltid bedre — og på en oppgave som eksplisitt ber om håndtering av manglende fil, er except FileNotFoundError: det som svarer på bestillingen.
For bred try er den samme feilen i en annen form: legger du hele analysen inn i try-blokken sammen med open, fanger du også feil i analysen. Hold try-blokken så kort som mulig — helst den ene linja som faktisk kan gå galt.
except: uten feiltype fanger alle unntak, inkludert dem som skyldes feil i din egen kode.Den er lovlig Python og gir bare et lite trekk i arkivets løsningsforslag, men den har to reelle kostnader: den skjuler ekte programmeringsfeil, og den gjør feilmeldingen din upresis.
Bokas regel: skriv alltid typen. Vet du ikke hvilken, kjør koden én gang og les siste linje i feilmeldingen — der står den.
Hva skriver programmet ut?
verdier = ['12', 'ni', '7']
total = 0
for v in verdier:
try:
total = total + int(v)
print('la til', v)
except ValueError:
print('hoppet over', v)
print('Total:', total)For hver situasjon: hvilken feiltype oppstår, og hvor?
a) f = open('finnes_ikke.txt', 'r')
b) int('dager') på overskriftsraden, fordi readline()-hoppet mangler
c) felt[2] på linja onsdag;svømming
d) d['Stavanger'] når ordboka bare har Bergen og Tromsø
e) sum(tall) / len(tall) når tall er tom
Løkke 3 — Den robuste filleseren (~12 min)
Nå setter vi det sammen til mønsteret eksamen faktisk ber om: en funksjon som leser fila hvis den kan, og som svarer pent hvis den ikke kan.
— naturlig pausepunkt: har du lest hit, har du det som trengs for deloppgaven om robusthet. Løkke 4 er finpussen. —
Når en funksjon ikke kan gjøre jobben sin, må den returnere noe kalleren kan kjenne igjen. Ellers flytter du bare krasjet ett hakk unna.
De tre formene boka bruker:
- None — «det finnes ikke noe svar». Brukes når fila mangler, eller når «beste rad» skal finnes i en tom tabell.
- [] eller {} — «tomt resultat». Brukes når kalleren skal kunne løkke videre uten å teste først.
- -1 — «ikke funnet», og bare i søkefunksjoner som ellers returnerer en indeks.
Velg én, og si i oppgaveteksten eller i en kommentar hvilken. Det viktigste er at kalleren kan teste: if tabell is None:.
def les_tabell(filnavn):
try:
f = open(filnavn, 'r')
except FileNotFoundError:
print('Fant ikke filen:', filnavn)
return None
tabell = []
for linje in f:
tabell.append(linje.strip().split(';'))
f.close()
return tabellUtskrift: ingen — blokken definerer bare funksjonen.
Fire faste deler:
1. try rundt open alene — den ene linja som kan utløse feilen.
2. except FileNotFoundError: — den spesifikke typen, ikke en naken except.
3. En melding som nevner filnavnet.
4. return None i feilgrenen, så kalleren kan teste if tabell is None:.
Resten av funksjonen er uendret fra kap. 4.1. Robusthet legges utenpå mønsteret du allerede kan — det er ikke en ny måte å lese filer på.
En try kan ha flere except-grener etter hverandre. Python velger den første som passer til feiltypen, og hopper over resten.
for tekst in ['maalinger.txt', 'salgstall.txt']:
try:
f = open(tekst, 'r')
tall = int(f.readline().strip())
f.close()
print('Første linje som tall:', tall)
except FileNotFoundError:
print('Fant ikke filen:', tekst)
except ValueError:
print('Første linje i', tekst, 'er ikke et tall')Utskrift:
Første linje i maalinger.txt er ikke et tall
Fant ikke filen: salgstall.txtBegge grenene ble brukt: den første fila finnes, men første linje er ikke et tall; den andre fila finnes ikke.
Rekkefølgen på grenene spiller bare rolle når typene overlapper. De fire du bruker i dette faget, overlapper ikke, så du kan skrive dem i den rekkefølgen som leser best.
Skriv les_logg(filnavn) som leser en treningslogg til en 2D-tabell med minutter som heltall. Fila kan mangle, og enkeltlinjer kan være ødelagte — de skal hoppes over med en melding, ikke stoppe innlesingen.
treningslogg-skadet.txt:mandag;løpetur;42
tirsdag;styrke;femtifem
onsdag;svømmingDen andre linja har femtifem i minuttfeltet, og den tredje mangler et felt. Fasiten fanger begge, hver med sin type:
def les_logg(filnavn):
try:
f = open(filnavn, 'r')
except FileNotFoundError:
print('Fant ikke filen:', filnavn)
return None
tabell = []
for linje in f:
felt = linje.strip().split(';')
try:
tabell.append([felt[0], felt[1], int(felt[2])])
except ValueError:
print('Hopper over linje med ugyldig tall:', linje.strip())
except IndexError:
print('Hopper over linje med for få felt:', linje.strip())
f.close()
return tabell
print(les_logg('treningslogg-skadet.txt'))
print(les_logg('finnes_ikke.txt'))Utskrift:
Hopper over linje med ugyldig tall: tirsdag;styrke;femtifem
Hopper over linje med for få felt: onsdag;svømming
[['mandag', 'løpetur', 42]]
Fant ikke filen: finnes_ikke.txt
NoneTo try-blokker med hvert sitt ansvar. Den ytre håndterer at fila mangler og avbryter alt. Den indre håndterer at en linje er ødelagt og hopper bare over den runden.
Resultatet er en tabell med de radene som lot seg lese — én av tre her — og en melding per rad som ble forkastet. Det er langt bedre enn både å krasje og å late som ingenting.
Slik ville sensor sett på det: den ytre håndteringen er det oppgaven vanligvis ber om. Den indre er et pluss som viser robusthet, og den koster fire linjer. En løsning som i stedet tester if len(felt) == 3: før konverteringen, er like riktig — flere korrekte veier sidestilles.
Denne funksjonen er ment å være robust, men har to feil:
def les_tall(filnavn):
try:
f = open(filnavn, 'r')
tall = []
for linje in f:
tall.append(int(linje.strip()))
f.close()
return tall
except:
print('Noe gikk galt')a) Hvilke to svakheter har den?
b) Skriv den om slik at manglende fil og ugyldige linjer håndteres hver for seg, og slik at kalleren alltid får noe den kan teste på.
Skriv hent_antall(d, by) som returnerer antallet for en by i ordboka, og som svarer 0 hvis byen ikke står der.
a) Skriv løsningen med try/except.
b) Skriv løsningen uten try/except.
c) Hvilken av dem passer best her, og hvorfor?
Løkke 4 — else, finally, og hvor try skal slutte (~10 min)
To grener til, og én regel som er viktigere enn begge.
Kjøres bare hvis ingen feil oppsto. Den brukes til å skille «det som kan gå galt» fra «det som skal skje når alt gikk bra».
for navn in ['maalinger.txt', 'salgstall.txt']:
try:
f = open(navn, 'r')
except FileNotFoundError:
print('Fant ikke filen:', navn)
else:
print('Åpnet', navn, 'med', len(f.readlines()), 'linjer')
f.close()Utskrift:
Åpnet maalinger.txt med 3 linjer
Fant ikke filen: salgstall.txtFordelen framfor å legge lesingen inn i try-blokken: du er sikker på at en feil i lesingen ikke blir forvekslet med en feil i åpningen.
else er nyttig, men aldri påkrevd — det samme kan skrives med koden etter try/except, slik mønsteret i løkke 3 gjør.
Kjøres alltid — enten det gikk bra eller galt. Den vanlige jobben er opprydding, og i dette faget betyr det å lukke fila.
f = open('maalinger.txt', 'r')
try:
tall = int(f.readline().strip())
print('Tall:', tall)
except ValueError:
print('Første linje er ikke et tall')
finally:
f.close()
print('Fila er lukket:', f.closed)Utskrift:
Første linje er ikke et tall
Fila er lukket: TrueLegg merke til at f.close() kjørte selv om int() feilet. Uten finally ville close() blitt hoppet over når feilen oppsto.
with open(...) (kap. 4.1) gjør det samme automatisk, og er den korteste veien til samme garanti.
Regelen som er viktigere enn både else og finally: try-blokken skal inneholde så få linjer som mulig — helst bare den ene som kan utløse feilen du vil fange.
Grunnen er at except fanger feilen uansett hvor i blokken den oppsto. Er blokken lang, fanger du feil du ikke mente å fange, og du får en feilmelding som peker på feil sted.
To praktiske utslag:
- try rundt open, ikke rundt hele innlesingen.
- try rundt konverteringen inne i løkka, ikke rundt hele løkka — da mister du bare den ene raden.
Den samme skrivefeilen — antal i stedet for antall — legges inn i to varianter av samme funksjon. Hva rapporterer hver av dem?
try, naken except: feilen svelges og meldingen er usann.def les_bred(filnavn):
try:
f = open(filnavn, 'r')
antall = 0
for linje in f:
antall = antal + 1
f.close()
return antall
except:
print('Kunne ikke lese fila')
return None
print(les_bred('maalinger.txt'))Utskrift:
Kunne ikke lese fila
NoneFila ble lest helt fint. Det som gikk galt, var skrivefeilen — men meldingen sier noe helt annet, og du kan lete lenge etter en filfeil som ikke finnes.
Snever try, spesifikk except: den samme skrivefeilen slipper igjennom til overflaten, der den hører hjemme.
# tilsiktet feil: skrivefeilen «antal» er med vilje
def les_snever(filnavn):
try:
f = open(filnavn, 'r')
except FileNotFoundError:
return None
antall = 0
for linje in f:
antall = antal + 1
f.close()
return antall
les_snever('maalinger.txt')Kodeblokken er merket som tilsiktet feilende. Siste linje i feilmeldingen er:
NameError: name 'antal' is not definedDet ser ut som et tilbakeskritt, men det er en forbedring. Programmet stopper på riktig linje, med riktig navn på feilen, og du finner den på ti sekunder.
Regelen i én setning: try skal fange det du ikke kan kontrollere — filer, brukerinput, ødelagte data. Den skal aldri fange dine egne skrivefeil.
Slik ville sensor sett på det: en besvarelse med bred, naken except får et lite trekk; en med snever, spesifikk håndtering får full uttelling og signaliserer at du vet hvorfor.
Hva skriver programmet ut?
def les(navn):
try:
f = open(navn, 'r')
except FileNotFoundError:
print('mangler')
return None
else:
print('åpnet')
finally:
print('ferdig med', navn)
f.close()
return 1
print(les('maalinger.txt'))
print(les('finnes_ikke.txt'))Fila maalinger.txt finnes; finnes_ikke.txt finnes ikke.
En værstasjon leverer maalinger.txt med linjer på formen sted;temperatur;fuktighet:
Innholdet i maalinger.txt:
Blindern;12.4;61
Kjevik;15.1;54
Værnes;9.8;73a) Skriv les_maalinger(filnavn) som returnerer en 2D-tabell med temperaturen som flyttall og fuktigheten som heltall. Manglende fil skal gi melding og None; ødelagte linjer skal hoppes over med melding.
b) Skriv snitt_temperatur(tabell) som returnerer gjennomsnittstemperaturen, og None for en tom tabell.
c) Skriv lagre_snitt(tabell, filnavn) som skriver snittet til fil med to desimaler — men som ikke skriver noen fil i det hele tatt hvis tabellen er tom.
d) Skriv main() som binder de tre sammen og som takler at fila mangler.
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.