Tilbake
4.2

4.2 Filskriving og lagring (tekst og `pickle`)

Skrive resultater til tekstfil, og lagre en hel datastruktur til binærfil med pickle — det vanlige siste leddet i kjeden.

45 min
6 oppgaver
Filskrivinglagring (tekst`pickle`)
Din fremgang i kapitlet
0 / 6 oppgaver
Forkunnskaper

- kap. 4.1 — filinnlesing: open, close og løkka som går gjennom linjene. Skriving er den samme mekanikken speilvendt.
- kap. 3.3 — strenger, særlig .join(liste) som setter feltene sammen igjen.
- kap. 3.4 — formatert utskrift: .rjust(), .format() og f-strenger. Det du skriver til fil, formateres på nøyaktig samme måte som det du skriver til skjerm.
- kap. 3.5 — ordbøker, som er strukturen pickle oftest lagrer.

Løkke 1 — Å skrive én linje om gangen (~10 min)

En resirkuleringsstasjon har telt innleverte glass per by, og skal levere fra seg en rapport. Analysen er gjort; det som gjenstår, er å få tallene ut av programmet og inn i en fil noen andre kan åpne.

Å skrive til fil er den samme mekanikken som å lese, med to forskjeller: du åpner med 'w' i stedet for 'r', og du må selv sette på linjeskiftet.

`open(filnavn, 'w')`

Åpner en fil for skriving. 'w' står for write.

To ting skjer med én gang, før du har skrevet et eneste tegn:

1. Finnes ikke fila, blir den opprettet.
2. Finnes den, blir den tømt. Alt som lå der, er borte.

Det er ingen advarsel og ingen angremulighet. Skal du beholde det som lå der fra før, er 'a' modusen du vil ha (neste løkke).

`f.write(tekst)`

Skriver en streng til fila på gjeldende posisjon. Den skriver nøyaktig det du gir den — ikke noe mer.

Særlig: write legger ikke på et linjeskift, slik print gjør. Vil du ha linjeskift, må du skrive det selv.

write tar bare strenger. Gir du den et tall, stopper programmet med TypeError — tallet må gjennom str() først.

Linjeskiftet `'\n'` i `write`

Regelen som skiller en lesbar fil fra en ubrukelig: hver linje du skriver, må avsluttes med '\n'.

Formen boka bruker er f.write(tekst + '\n') — linjeskiftet skjøtes på i samme kall, så du ikke kan glemme det.

Uten linjeskiftet havner alt du skriver etter hverandre på én eneste lang linje. Fila er ikke ødelagt, men den er umulig å lese tilbake med for linje in f:, fordi det bare finnes én linje.

`f.close()` ved skriving

Lukker fila — og det er viktigere når du skriver enn når du leser.

Det du skriver, samles opp i en buffer i minnet og flyttes over til disken i porsjoner. close() tømmer bufferet. Glemmer du den, kan de siste linjene mangle i fila, selv om programmet ditt ikke gjorde noe galt.

Regelen er den samme som ved lesing: hver open har en close, og den står etter løkka som skriver.

✏️Eksempel 1: Skriv tre linjer og les dem tilbake

Skriv de tre byene og glasstallene til rapport.txt, én linje per by. Les deretter fila tilbake og skriv den ut, som en kontroll på at det faktisk ble tre linjer.

Kontrollen er hele poenget: den eneste måten å vite at skrivingen ble riktig, er å lese resultatet tilbake.

tall = [['Bergen', 120], ['Tromsø', 45], ['Stavanger', 88]]

f = open('rapport.txt', 'w')
for rad in tall:
    f.write(rad[0] + ';' + str(rad[1]) + '\n')
f.close()

f = open('rapport.txt', 'r')
for linje in f:
    print(linje.strip())
f.close()

Utskrift:

Bergen;120
Tromsø;45
Stavanger;88

Tre detaljer gir uttelling hver for seg. str(rad[1]) fordi write ikke tar imot tall. + '\n' fordi write ikke legger på linjeskift. Og close() etter løkka, som tømmer bufferet til disk.

Legg merke til at fila åpnes to ganger. Skriving og lesing er to helt adskilte operasjoner, med hver sin modus. Du kan ikke lese fra en fil du åpnet med 'w'.

Slik ville sensor sett på det: løkka som skriver én linje per rad er kjernen. En løsning som bygger hele teksten i en streng først og skriver den med ett write-kall, gir like full pott — det er én av flere korrekte veier.

📝Oppgave 1

(Innstegsoppgave — sjanger I, utskrift og lagring.) Se på denne kodesnutten:

f = open('resultat.txt', 'w')
f.write('Bergen')
f.write(120)
f.close()

a) Hvilken av de to write-linjene stopper programmet, og hvorfor?

b) Skriv om begge linjene slik at fila får linja Bergen;120 etterfulgt av et linjeskift.

Løkke 2 — Skriv en hel tabell tilbake til fil (~12 min)

Den vanlige bestillingen på eksamen er: «lagre den bearbeidede tabellen i en ny fil, på samme format som den du leste».

Da skal hver rad bli én linje, med separatoren mellom feltene. Det er nøyaktig motsatt av parsingen i kap. 4.1: der delte du en linje i felt, her setter du felt sammen til en linje.

`str(x)`

Gjør et tall (eller hva som helst annet) om til den strengen print ville vist.

print(str(120) + ' glass')
print(str(12.4))
print(str(['a', 'b']))

Utskrift:

120 glass
12.4
['a', 'b']

Du trenger str hver gang et tall skal skjøtes sammen med tekst med +, og hver gang et tall skal inn i write. Det er den motsatte veien av int() og float() fra kap. 4.1.

`';'.join(liste)`

Setter elementene i en liste sammen til én streng, med separatoren imellom. Den er det nøyaktige speilbildet av .split(';').

rad = ['Bergen', '120']
print(';'.join(rad))
print(', '.join(['ost', 'brød', 'melk']))
print(';'.join(rad).split(';'))

Utskrift:

Bergen;120
ost, brød, melk
['Bergen', '120']

Alle elementene må være strenger. Er ett av dem et tall, stopper programmet med TypeError — konverter med str() i en løkke først.

Siste linje viser rundturen: join og split opphever hverandre. Det er derfor formatet du skriver, kan leses tilbake av den samme funksjonen som leste originalfila.

📜Kodemønster: skriv 2D-tabell til fil
def lagre_tabell(tabell, filnavn):
    f = open(filnavn, 'w')
    for rad in tabell:
        felt = []
        for verdi in rad:
            felt.append(str(verdi))
        f.write(';'.join(felt) + '\n')
    f.close()

Utskrift: ingen — blokken definerer bare funksjonen.

Fem faste deler:

1. open med 'w' før løkka.
2. En løkke per rad, ikke per felt.
3. str() på hvert felt — tabellen inneholder tall etter konverteringen ved innlesing.
4. ';'.join(felt) + '\n' — separator mellom feltene, linjeskift etter linja.
5. close etter løkka.

Funksjonen returnerer ingenting. Det er riktig: den har en virkning (fila blir til), ikke en verdi.

✏️Eksempel 2: Lagre en bearbeidet tabell

Glasstallene skal doblesjekkes mot pant: hver by får en ny kolonne med pantbeløpet, 3 kroner per glass. Skriv den utvidede tabellen til pant.txt på formen by;antall;beløp, og les fila tilbake for å kontrollere.

tall = [['Bergen', 120], ['Tromsø', 45], ['Stavanger', 88]]

utvidet = []
for rad in tall:
    utvidet.append([rad[0], rad[1], rad[1] * 3])

f = open('pant.txt', 'w')
for rad in utvidet:
    felt = []
    for verdi in rad:
        felt.append(str(verdi))
    f.write(';'.join(felt) + '\n')
f.close()

f = open('pant.txt', 'r')
print(f.read().strip())
f.close()

Utskrift:

Bergen;120;360
Tromsø;45;135
Stavanger;88;264

Den indre løkka som gjør str() på hvert felt er ikke pynt. Uten den stopper join med TypeError, fordi to av tre felt er heltall.

Fila kan nå leses tilbake av les_tabell fra kap. 4.1 uten en eneste endring — samme separator, samme linjeskift, samme antall felt. Det er hele poenget med å skrive på det formatet du leste.

Slik ville sensor sett på det: en løsning som bruker en listekomprehensjon ([str(v) for v in rad]) eller som skriver f.write(rad[0] + ';' + str(rad[1]) + ';' + str(rad[2]) + '\n') direkte, gir like full pott. Den siste er bare mindre robust, fordi den låser antall kolonner.

📝Oppgave 2
Eksamensnivå, sjanger I

En treningslogg ligger som 2D-tabell i programmet:

logg = [['mandag', 'løpetur', 42], ['tirsdag', 'styrke', 55], ['onsdag', 'svømming', 30]]

a) Skriv lagre_logg(tabell, filnavn) som skriver hver rad som en semikolonseparert linje.

b) Utvid funksjonen slik at den først skriver overskriftsraden dag;økt;minutter.

c) Hvorfor returnerer funksjonen ingenting?

📝Oppgave 3
Eksamensnivå, sjanger B

Hva skriver programmet ut?

f = open('kort.txt', 'w')
f.write('a')
f.write('b\n')
f.write('c')
f.close()

f = open('kort.txt', 'r')
linjer = f.readlines()
f.close()
print(len(linjer))
print(linjer)

Løkke 3 — Legg til i stedet for å overskrive, og skriv en pen rapport (~10 min)

To ting gjenstår på tekstsiden: modusen som legger til i stedet for å tømme, og formateringen som gjør en rapport lesbar for et menneske.

`open(filnavn, 'a')`

Åpner fila for tilføying (append). Alt du skriver, havner bakerst; det som lå der fra før, blir stående. Finnes ikke fila, opprettes den.

f = open('logg2.txt', 'w')
f.write('første økt\n')
f.close()

f = open('logg2.txt', 'a')
f.write('andre økt\n')
f.close()

f = open('logg2.txt', 'r')
print(f.read().strip())
f.close()

Utskrift:

første økt
andre økt

Valget mellom 'w' og 'a' er et innholdsvalg, ikke et teknisk et: skal fila være en fasit som lages på nytt hver gang ('w'), eller en logg som vokser ('a')? Les oppgaveteksten, den sier det alltid.

`with open(filnavn, 'w') as f:`

Samme automatiske lukking som ved lesing, og her betyr den mer: bufferet tømmes selv om programmet stopper med en feil midt i skrivingen.

with open('kvittering.txt', 'w') as f:
    f.write('Sum: 253\n')

with open('kvittering.txt', 'r') as f:
    print(f.read().strip())

Utskrift:

Sum: 253

Boka bruker likevel openclose som hovedform, fordi det er den arkivets løsningsforslag bruker. with er fullt gyldig og gir full pott.

`'{:.2f}'.format(x)`

Formaterer et tall som en streng med et bestemt antall desimaler eller en bestemt bredde. Formen er den arkivets løsningsforslag bruker.

print('{:.2f}'.format(12.43333))
print('{:6.2f}|'.format(12.4))
print('{} har {} glass'.format('Bergen', 120))

Utskrift:

12.43
 12.40|
Bergen har 120 glass

.2f er to desimaler, 6.2f er to desimaler i et felt som er seks tegn bredt (høyrejustert). Tallet avrundes, det klippes ikke.

Formatering hører hjemme ved utskrift og lagring, ikke ved beregning: regn med full presisjon, og rund av først når mennesket skal lese tallet.

f-streng: `f'{x:.2f}'`

Den nyere skrivemåten for det samme. Prefikset f foran fnutten gjør at det som står i klammene, regnes ut og settes inn.

x = 12.43333
navn = 'Bergen'
print(f'{x:.2f}')
print(f'{navn} har {120} glass')
print(f'{navn:<12}|{x:6.2f}|')

Utskrift:

12.43
Bergen har 120 glass
Bergen      | 12.43|

Formatspråket etter kolonet er identisk med det i .format().2f, 6.2f, <12 betyr det samme begge steder.

f-strenger er varslet inn i pensum i det nyeste løsningsforslaget i arkivet (verifiser mot gjeldende pensum — varslet inn 2019). Den eldre .format()-formen er alltid gyldig, og begge gir full pott. Bruk den du husker best.

`.rjust(n)` og `.ljust(n)`

Fyller ut en streng med mellomrom til den er n tegn bred — rjust mot høyre (tall), ljust mot venstre (tekst).

print('Bergen'.ljust(12) + str(120).rjust(5))
print('Tromsø'.ljust(12) + str(45).rjust(5))
print('Stavanger'.ljust(12) + str(88).rjust(5))

Utskrift:

Bergen        120
Tromsø         45
Stavanger      88

Er strengen allerede bredere enn n, skjer ingenting — den blir aldri klippet.

Dette er den enkleste veien til en rapport med rette kolonner, og den virker like godt mot fil som mot skjerm: f.write(linje + '\n') i stedet for print(linje).

✏️Eksempel 3: En formatert rapport til fil

Skriv en rapport til pantrapport.txt med en overskriftslinje, én linje per by med byen venstrejustert og beløpet høyrejustert med to desimaler, og en sumlinje til slutt. Les fila tilbake og skriv den ut.

tall = [['Bergen', 120], ['Tromsø', 45], ['Stavanger', 88]]

f = open('pantrapport.txt', 'w')
f.write('By'.ljust(12) + 'Pant'.rjust(9) + '\n')
sum_pant = 0
for rad in tall:
    pant = rad[1] * 2.5
    sum_pant = sum_pant + pant
    f.write(rad[0].ljust(12) + '{:9.2f}'.format(pant) + '\n')
f.write('Sum'.ljust(12) + '{:9.2f}'.format(sum_pant) + '\n')
f.close()

f = open('pantrapport.txt', 'r')
print(f.read().strip())
f.close()

Utskrift:

By               Pant
Bergen         300.00
Tromsø         112.50
Stavanger      220.00
Sum            632.50

Kolonnene står rett fordi bredden er den samme på alle linjene — 12 tegn til navnet, 9 til beløpet. Det er hele trikset; det finnes ingen usynlig tabellmotor.

Den samme rapporten med f-strenger, som gir nøyaktig samme fil:

tall = [['Bergen', 120], ['Tromsø', 45], ['Stavanger', 88]]
for rad in tall:
    pant = rad[1] * 2.5
    print(f'{rad[0]:<12}{pant:9.2f}')

Utskrift:

Bergen         300.00
Tromsø         112.50
Stavanger      220.00

Begge former gir full pott. (verifiser mot gjeldende pensum — f-strenger ble varslet inn 2019); arkivets egne løsningsforslag bruker .format() og .rjust().

Slik ville sensor sett på det: poengene ligger på at hver rad blir én linje, at tallene er formatert, og at sumlinja er regnet ut i samme løkke. Nøyaktig kolonnebredde er ikke poenggivende — «pen utskrift» er kravet, ikke bestemte tall.

📝Oppgave 4
Eksamensnivå, sjanger I

Du har tabellen

maalinger = [['Blindern', 12.4], ['Kjevik', 15.1], ['Værnes', 9.8]]

a) Skriv lagre_snitt(tabell, filnavn) som legger til én linje bakerst i fila på formen Snitt: 12.43, med to desimaler, uten å slette det som ligger der fra før.

b) Hvorfor er 'a' riktig modus her, og hva ville skjedd med 'w'?

c) Skriv den samme formateringen med f-streng.

Løkke 4 — pickle: lagre en hel datastruktur (~8 min)

Bør kjenne til — nivå 3 av bokas tre prioritetsnivåer. pickle står i ~30 % av settene (3 av 10 sittinger) og i funksjonsoversikten som deles ut på eksamen hvert år, men den er aldri hovedsaken i en oppgave. Har du kort tid, les denne løkka sist — og les den, for den er kort og gir billige poeng når den først kommer.

Problemet den løser: du har en ordbok med hundre nøkler, og du vil lagre den og få den tilbake slik den var neste gang programmet kjører. Med tekstfil må du skrive den ut i et format og parse den tilbake selv — og da må du bestemme hva som skal skje med tallene, med lister inni verdiene, med nøkler som inneholder semikolon. Med pickle slipper du alt det.

Tekstfil og binærfil

En tekstfil inneholder tegn som mennesker kan lese, delt i linjer. Alle filene så langt i boka har vært tekstfiler.

En binærfil inneholder rå byte som er ment for programmer, ikke for øyne. Åpner du den i en teksteditor, ser du søppel. Bildefiler, lydfiler og pickle-filer er binærfiler.

Skillet får to praktiske konsekvenser: binærfiler åpnes med 'b' i modusen ('wb', 'rb'), og innholdet i dem vises aldri i denne boka — det ville ikke betydd noe.

Serialisering
Serialisering er å gjøre en datastruktur i minnet om til en byterekke som kan lagres eller sendes, og deserialisering er å bygge strukturen opp igjen fra byterekka.

Poenget er at strukturen kommer tilbake identisk: en ordbok med lister inni, med tall som fortsatt er tall, forblir nøyaktig det. Ingen parsing, ingen konvertering, ingen tapte typer.

I Python heter verktøyet pickle, og de to operasjonene heter dump (lagre) og load (hente).

`import pickle`

Henter inn standardmodulen pickle. Linja står øverst i programmet, og uten den finnes ikke navnet pickle.

pickle er en del av Python og trenger ingen installasjon. Det er en av de svært få modulene denne boka bruker i det hele tatt — resten av faget klarer seg med språket selv.

`open(filnavn, 'wb')` og `open(filnavn, 'rb')`

Åpner en fil i binærmodus for henholdsvis skriving og lesing. b-en er det eneste som skiller dem fra 'w' og 'r'.

Bruker du 'w' der 'wb' skal stå, stopper programmet med TypeError: write() argument must be str, not bytes — en feilmelding som er lett å kjenne igjen når du først har sett den.

Regelen er enkel: pickle krever alltid b i modusen, begge veier.

`pickle.dump(objekt, fil)`

Skriver hele objektet — ordbok, liste, 2D-tabell, tuppel — til en åpen binærfil, i én operasjon.

Rekkefølgen på argumentene er objektet først, fila etterpå. Bytter du om, stopper programmet.

Du kan lagre nøstede strukturer (en ordbok av lister av tall) uten å tenke på formatet i det hele tatt. Det er hele salgsargumentet.

`pickle.load(fil)`

Leser objektet tilbake fra en åpen binærfil og returnerer det. Strukturen er identisk med den som ble lagret: samme nøkler, samme typer, samme nesting.

Ett argument, fila. Husk å ta imot returverdien — pickle.load(f) uten tilordning kaster resultatet.

Er fila skrevet av dump, virker load. Er den ikke det, stopper programmet — pickle leser bare sitt eget format.

✏️Eksempel 4: Lagre en ordbok og hente den tilbake

Lagre ordboka {'Bergen': [120, 300.0], 'Tromsø': [45, 112.5]} til binærfila beholdning.pkl, hent den tilbake i et nytt oppslag, og vis at den kom tilbake identisk — med tallene fortsatt som tall.

import pickle

beholdning = {'Bergen': [120, 300.0], 'Tromsø': [45, 112.5]}

f = open('beholdning.pkl', 'wb')
pickle.dump(beholdning, f)
f.close()

f = open('beholdning.pkl', 'rb')
hentet = pickle.load(f)
f.close()

print(hentet)
print(hentet == beholdning)
print(hentet['Bergen'][0] + 1)

Utskrift:

{'Bergen': [120, 300.0], 'Tromsø': [45, 112.5]}
True
121

Den midterste linja i utskriften er beviset: strukturen som kom ut, er lik den som gikk inn.

Den siste er den viktige. hentet['Bergen'][0] + 1 gir 121, ikke 1201. Tallet er fortsatt et tall — ingen int() var nødvendig. Det er den ene tingen pickle gir deg som en tekstfil ikke gjør.

Innholdet i beholdning.pkl vises ikke noe sted i denne boka. Det er en binærfil: åpner du den i en teksteditor, ser du uleselige tegn, og det er meningen.

Slik ville sensor sett på det: de fire poenggivende punktene er import pickle, 'wb'/'rb', argumentrekkefølgen i dump, og at load sin returverdi tas imot. Dette er nivå 3-stoff — kommer det, er det verdt få prosent, og det er den enkleste deloppgaven i settet.

📝Oppgave 5
Sjanger I

Denne koden er ment å lagre en liste og hente den tilbake, men har to feil:

import pickle

data = [3, 1, 4]
f = open('tall.pkl', 'w')
pickle.dump(f, data)
f.close()

a) Hvilke to feil er det?

b) Skriv koden riktig, og hent lista tilbake for å vise at den kom uendret ut.

📝Oppgave 6
Eksamensnivå, sjanger I

Et resirkuleringsprogram har talt opp glass per by i ordboka antall og skal levere fra seg to filer.

a) Skriv lagre_rapport(antall, filnavn) som skriver én linje per by på formen by;antall, sortert alfabetisk på by.

b) Skriv lagre_binaert(antall, filnavn) som lagrer hele ordboka med pickle.

c) Skriv hent_binaert(filnavn) som henter ordboka tilbake, og forklar med én setning hvorfor rapporten i a) ikke kan brukes til det samme uten videre.

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.