Tilbake
8.2

8.2 Filskriving, filtrering og fast-bredde-felt

Skrive en datastruktur tilbake til fil, filtrere uten å endre argumentet, og lese fast-bredde-felt med posisjonsskiving.

50 min
7 oppgaver
Filskrivingfiltreringfast-bredde-felt
Din fremgang i kapitlet
0 / 7 oppgaver

Forkunnskaper — sist du var her

Fra kap. 8.1 tar vi med oss den nøstede ordboka og to måter å gå gjennom den på. Her er de oppfrisket med kjørt utskrift.

1. Strukturen vi skriver ut og filtrerer. Ytre nøkkel er stasjonsnavnet, indre nøkler er feltnavnene.

data = {'Kvitheia': {'temp': 7.9, 'nedbor': 1180},
        'Nordvika': {'temp': 3.1, 'nedbor': 980}}
for navn, verdier in data.items():
    print(f'{navn:10s}{verdier["nedbor"]:6d}')

Utskrift:

Kvitheia    1180
Nordvika     980

.items() gir nøkkel og verdi samtidig, og f-strengene f'{navn:10s}' og f'{...:6d}' gir fast feltbredde. Nøyaktig samme f-strenger brukes når linjene skal skrives til fil.

2. Innlesingsmønsteret vi bygger videre på. Denne funksjonen fra kap. 8.1 gjenbrukes flere steder i kapitlet, så du kan slå tilbake hit:

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:

4

Datafila er den samme som i forrige kapittel, og du kan lage den selv på to sekunder — en tekstfil stasjoner.txt med disse fire linjene i samme mappe som programmet:

Kvitheia;7.9;1180
Sandvollen;5.4;760
Ospedalen;8.6;1420
Nordvika;3.1;980

Trenger du å friske opp f-strenger, står de i kap. 1.4; ordbøker i kap. 1.5.

Løkke 1 — Skrive en datastruktur til fil (~15 min)

Et program som leser målinger, regner ut noe og bare skriver svaret i terminalen, har mistet resultatet i det du lukker vinduet. Derfor ber eksamensoppgaven deg lagre. Skrivingen er kortere enn lesingen: åpne fila i skrivemodus, og skriv én streng per linje.

Ett grep skiller seg fra alt annet du har gjort med print: du må sette linjeskiftet inn selv.

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

Åpner fila for skriving. Det andre argumentet er modusen: 'w' betyr write. Finnes ikke fila, opprettes den. Finnes den, blir innholdet slettet i samme øyeblikk fila åpnes — også hvis du ikke skriver noe etterpå. Det er ikke en advarsel om noe uvanlig; det er den normale oppførselen, og den er verdt å vite når du tester koden på ekte data.

Som ved lesing lukkes fila automatisk når with-blokken slutter. Navnet outfile er konvensjonen sensor forventer.

with open('demo3.txt', 'w') as outfile:
    outfile.write('første kjøring\n')
with open('demo3.txt', 'w') as outfile:
    outfile.write('andre kjøring\n')
with open('demo3.txt') as infile:
    n = 0
    for line in infile:
        n += 1
        print(line, end='')
print(n)

Utskrift:

andre kjøring
1

Den første linja er borte. Fila inneholder bare det siste with-blokken skrev.

`outfile.write(tekst)`

Skriver strengen tekst til fila, nøyaktig slik den står — uten å legge til linjeskift. Vil du ha én linje per oppføring, må du selv avslutte med '\n'. Det er den ene forskjellen fra print, og den vanligste feilen i deloppgaven.

write godtar bare strenger. Har du et tall, må det konverteres med str(...) eller settes inn i en f-streng; ellers får du TypeError.

with open('demo2.txt', 'w') as outfile:
    outfile.write(1180)

Siste linje i feilmeldingen er:

Utskrift:

TypeError: write() argument must be str, not int

✏️Eksempel 1: skriv en ordbok til fil, og les den tilbake

En ordbok knytter stasjonsnavn til årsnedbør. Skriv den til sum.txt med semikolon mellom feltene, én stasjon per linje, og vis at fila ble som tenkt.

Vi skriver med str(...) rundt tallet, og leser fila tilbake med lesemønsteret fra forrige kapittel. Å lese tilbake er den eneste måten å se at skrivingen ble riktig, og en god vane når du tester.

data = {'Kvitheia': 1180, 'Sandvollen': 760, 'Ospedalen': 1420}

with open('sum.txt', 'w') as outfile:
    for navn in data:
        outfile.write(navn + ';' + str(data[navn]) + '\n')

with open('sum.txt') as infile:
    for line in infile:
        print(line, end='')

Utskrift:

Kvitheia;1180
Sandvollen;760
Ospedalen;1420

print(line, end='') brukes fordi line alt har linjeskiftet i seg — hadde vi brukt vanlig print, ville det blitt en blank linje mellom hver stasjon. Merk at fila står i innsettingsrekkefølgen til ordboka.

📝Oppgave 1

(Midtveisnivå, sjanger A — kodesporing, altså «hva skriver dette programmet ut?».) Programmet skriver til en fil og leser den tilbake. Hva blir utskriften?

data = {'Nordvika': 980, 'Kvitheia': 1180}
with open('x1.txt', 'w') as outfile:
    for navn in data:
        outfile.write(navn + ' ' + str(data[navn]) + '\n')

with open('x1.txt') as infile:
    for line in infile:
        print(line, end='')

Formatert linje til fil: `outfile.write(f'…\n')`

Den idiomatiske skrivemåten når linja har flere felt: bygg hele linja som en f-streng, med separator og linjeskift inne i strengen, og send den til write i én bevegelse. Formatspesifikatorene er de samme som i print: f'{x:.1f}' gir én desimal, f'{n:d}' gir et heltall, f'{navn:10s}' gir fast tekstbredde.

Fordelen er at antall desimaler i fila blir kontrollert. Uten formatering skriver Python flyttall med alle sifrene sine, og filer med 7.900000000000001 er vondt å lese.

d = {'Vest': {'ph': 7.4}, 'Aust': {'ph': 8.1}}
for stasjon, verdier in d.items():
    print(f'{stasjon};{verdier["ph"]:.2f}')

Utskrift:

Vest;7.40
Aust;8.10

Her går linjene til skjermen for å vises, men strengene er nøyaktig de samme du ville sendt til write — med '\n' lagt til på slutten.

✏️Eksempel 2: skriv en nøstet ordbok med header-linje

Skriv en nøstet ordbok til rapport.txt slik at fila kan leses tilbake av innlesingsmønsteret fra forrige kapittel: én header-linje med kolonnenavn, deretter én linje per stasjon med semikolon mellom feltene.

Headeren skrives én gang, før løkka. Deretter én f-streng per stasjon.

data = {'Kvitheia': {'temp': 7.9, 'nedbor': 1180},
        'Nordvika': {'temp': 3.1, 'nedbor': 980}}

with open('rapport.txt', 'w') as outfile:
    outfile.write('stasjon;temp;nedbor\n')
    for navn, verdier in data.items():
        outfile.write(f'{navn};{verdier["temp"]:.1f};{verdier["nedbor"]:d}\n')

with open('rapport.txt') as infile:
    for line in infile:
        print('[' + line.strip() + ']')

Utskrift:

[stasjon;temp;nedbor]
[Kvitheia;7.9;1180]
[Nordvika;3.1;980]

Fila er nå symmetrisk med innlesingen: skriver du en header, må leseren hoppe over den med readline(). Skriver du ingen, må leseren ikke gjøre det. Nevn valget i en kommentar hvis oppgaveteksten ikke sier hva som forventes — det er en rimelig antakelse som skal dokumenteres, og sensor godtar begge så lenge du er konsekvent.

📝Oppgave 2
Eksamensnivå, sjanger L

Gitt en nøstet ordbok med stasjonsnavn som nøkkel og {'temp': ..., 'nedbor': ...} som verdi.

a) Skriv skriv_til_fil(data, filnavn) som lagrer strukturen med en header-linje stasjon;temp;nedbor og deretter én semikolonseparert linje per stasjon.

b) Kall funksjonen på en ordbok med Kvitheia (7.9, 1180) og Ospedalen (8.6, 1420), og les fila tilbake for å vise resultatet.

Løkke 2 — Filtrering uten å ødelegge originalen (~20 min)

— naturlig pausepunkt —

Del (c) lyder nesten alltid slik: «skriv en funksjon som returnerer en ny ordbok med bare de oppføringene som oppfyller et krav, uten å endre argumentet». De to uthevede ordene er hele oppgaven, og de er der fordi det er det man gjør galt.

Grunnen til at kravet stilles, er praktisk: den som kaller funksjonen din, skal fortsatt ha hele datasettet etterpå. En filterfunksjon som spiser originalen, er ubrukelig i et program som skal filtrere på flere grenser.

📜Filtreringsmønsteret: bygg og returner en ny ordbok

Fire steg, og de er alltid de samme:

1. Lag en tom ordbok: ny = {}.
2. Gå gjennom originalen med for nøkkel, verdi in data.items():.
3. Test betingelsen med if, og sett bare de kvalifiserte inn i ny.
4. return ny — utenfor løkka.

Originalen leses, aldri skrives. I kode:

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}

Nederste linje er beviset: originalen er urørt. Nettopp den utskriften er det oppgaven ber deg vise, og sensor ser etter tre ting — at ny opprettes tom, at betingelsen står i en if inne i løkka, og at return står utenfor.

`ny = {}` og `for nøkkel, verdi in data.items():`

Filtreringens to faste linjer. Den tomme ordboka ny er der resultatet samles; .items() gir nøkkel og verdi samtidig, slik at betingelsen kan stilles på verdien og nøkkelen tas med videre.

Er verdien selv en ordbok (nøstet struktur), stiller du betingelsen på et av de indre feltene: if verdier['nedbor'] > grense:. Skriv da inn en ny indre ordbok med de feltene du vil beholde — det gjør resultatet helt uavhengig av originalen.

data = {'Kvitheia': {'temp': 7.9}, 'Nordvika': {'temp': 3.1}}
ny = {}
for navn, verdier in data.items():
    if verdier['temp'] > 5.0:
        ny[navn] = {'temp': verdier['temp']}
print(ny, len(data))

Utskrift:

{'Kvitheia': {'temp': 7.9}} 2

✏️Eksempel 3: `mye_nedbor` — filtrer en nøstet ordbok

Skriv mye_nedbor(data, grense) som returnerer en ny nøstet ordbok med bare stasjonene som har mer nedbør enn grense. Argumentet skal ikke endres. Vis at originalen fortsatt har alle fire stasjonene.

Den indre ordboka bygges på nytt, felt for felt, slik at hverken den ytre eller de indre ordbøkene deles med originalen.

def mye_nedbor(data, grense):
    ny = {}
    for navn, verdier in data.items():
        if verdier['nedbor'] > grense:
            ny[navn] = {'temp': verdier['temp'], 'nedbor': verdier['nedbor']}
    return ny


data = {'Kvitheia': {'temp': 7.9, 'nedbor': 1180},
        'Sandvollen': {'temp': 5.4, 'nedbor': 760},
        'Ospedalen': {'temp': 8.6, 'nedbor': 1420},
        'Nordvika': {'temp': 3.1, 'nedbor': 980}}

vaate = mye_nedbor(data, 1000)
print(vaate)
print(len(data), len(vaate))

Utskrift:

{'Kvitheia': {'temp': 7.9, 'nedbor': 1180}, 'Ospedalen': {'temp': 8.6, 'nedbor': 1420}}
4 2

4 2 er det tallparet oppgaven vil se: fire i originalen, to i resultatet. Merk at grensen er streng (>), så en stasjon med nøyaktig 1000 mm ville falt utenfor. Sier oppgaveteksten «minst», skal det være >= — les kravet ordrett, og skriv en kommentar hvis det er tvetydig.

📝Oppgave 3
Eksamensnivå, sjanger L

Bruk samme struktur som i Eksempel 3.

a) Skriv kalde(data, grense) som returnerer en ny nøstet ordbok med bare stasjonene der 'temp' er lavere enn grense. Argumentet skal være urørt etterpå.

b) Kall funksjonen med grensen 6.0 på Kvitheia (7.9, 1180), Sandvollen (5.4, 760) og Nordvika (3.1, 980), og skriv ut både resultatet og antall oppføringer i originalen.

Aliasing: `ny[navn] = verdier` deler den indre ordboken

En finere variant av samme felle. Setter du inn hele verdien slik den står — ny[navn] = verdier — får du en ny ytre ordbok, men de indre ordbøkene er de samme objektene som i originalen. Endrer noen et indre felt i resultatet, endres originalen også.

For eksamen er dette godt nok: oppgaven ber om en ny ordbok, og en ny ytre ordbok er en ny ordbok. Men skal resultatet være helt uavhengig, bygger du den indre på nytt: ny[navn] = {'temp': verdier['temp'], 'nedbor': verdier['nedbor']}.

data = {'Kvitheia': {'nedbor': 1180}, 'Nordvika': {'nedbor': 980}}
ny = {}
for navn, verdier in data.items():
    if verdier['nedbor'] > 900:
        ny[navn] = verdier
ny['Kvitheia']['nedbor'] = 0
print(ny)
print(data)

Utskrift:

{'Kvitheia': {'nedbor': 0}, 'Nordvika': {'nedbor': 980}}
{'Kvitheia': {'nedbor': 0}, 'Nordvika': {'nedbor': 980}}

Nullen slo gjennom til originalen, selv om den ytre ordboka var ny.

📝Oppgave 4

(Midtveisnivå, sjanger A — kodesporing.) Hva skriver dette programmet ut?

def rens(data):
    for navn in data:
        data[navn] = data[navn] * 2
    return data

d = {'a': 3, 'b': 5}
e = rens(d)
print(e)
print(d)
print(len(d), len(e))

📝Oppgave 5
Eksamensnivå, sjanger L

Fila stasjoner.txt er den fra Forkunnskaper: navn;temp;nedbor, uten header.

a) Bruk les_stasjoner fra kap. 8.1 og skriv kalde_stasjoner(data, grense) som returnerer en ny ordbok med stasjonene under en temperaturgrense, uten å endre argumentet.

b) Skriv skriv(data, filnavn) som lagrer en slik ordbok til fil med linjer navn;temp og temperaturen med én desimal.

c) Kjør hele kjeden med grensen 5.0: skriv ut den filtrerte ordboka, antall oppføringer i original og resultat, og innholdet i fila.

Løkke 3 — Fast-bredde-felt (bør kjenne til) (~10 min)

Dette delkapitlet er merket «bør kjenne til». Det har vært innom ett av de gjennomgåtte settene og teller få poeng. Les det når resten av kapitlet sitter, ikke før.

Noen datafiler har ingen separator. Feltene står i stedet i faste kolonner, slik målestasjoner og observatorier har lagret data i femti år: tegn 1–11 er navnet, tegn 12–19 er datoen, og så videre. Da kan ikke .split() brukes til noe — det finnes ikke noe skilletegn å dele på. I stedet klipper du ut tegnposisjoner med slicing.

Fila under bruker faste kolonner. Du kan lage den selv, men vær nøye: mellomrommene er data her, så antall mellomrom må stemme.

Innholdet i dybder.txt:

Kvitheia   20240301   4.2  118
Sandvollen 20240301  -1.8   43
Ospedalen  20240302   6.5  205

Hver linje er nøyaktig 30 tegn: navn i posisjon 0–10, dato i 11–18, temperatur i 19–24 og snødybde i 25–29.

`line[a:b]` — fast-bredde-felt

Slicing på linja klipper ut tegnene fra posisjon a til og med b-1 — slutten er eksklusiv, som alltid ved slicing. Resultatet er en streng som typisk har mellomrom rundt seg, så .strip() må på hvert utsnitt før du bruker det. Tallfelt må i tillegg konverteres.

Vær oppmerksom på at Python teller fra 0, mens kolonnebeskrivelser i dokumentasjon ofte teller fra 1. «Feltet står i kolonne 1–11» blir line[0:11] i Python.

line = 'Kvitheia   20240301   4.2  118\n'
print('[' + line[0:11] + ']')
print('[' + line[11:19] + ']')
print('[' + line[19:25] + ']')
print('[' + line[25:30] + ']')

Utskrift:

[Kvitheia   ]
[20240301]
[   4.2]
[  118]

Merk at float(' 4.2') og int(' 118') fungerer selv uten .strip() — tallkonvertering tåler blanktegn. Det er tekstfeltene som må strippes.

✏️Eksempel 4: les en kolonnefil til nøstet ordbok

Les dybder.txt til en ordbok med stasjonsnavn som ytre nøkkel, dato som neste nøkkel, og {'temp': ..., 'dybde': ...} innerst.

Fire slicing-uttrykk erstatter .split(). Resten av mønsteret — den ytre sjekken, typekonverteringen — er identisk med det du kjenner.

data = {}
with open('dybder.txt') as infile:
    for line in infile:
        navn = line[0:11].strip()
        dato = line[11:19]
        temp = float(line[19:25])
        dybde = int(line[25:30])
        if navn not in data:
            data[navn] = {}
        data[navn][dato] = {'temp': temp, 'dybde': dybde}

print(data['Sandvollen'])
print(data['Ospedalen']['20240302']['dybde'])

Utskrift:

{'20240301': {'temp': -1.8, 'dybde': 43}}
205

Bare navnet trenger .strip(); datoen fyller hele feltet sitt, og tallene konverteres uansett. Å skrive .strip() på alle fire er likevel trygt og koster ingenting.

📝Oppgave 6

(Bør kjenne til, sjanger L — fast-bredde-felt.) Bruk dybder.txt fra dette delkapitlet, med snødybden i posisjon 25–29.

a) Skriv koden som summerer snødybden over alle tre linjene og skriver ut summen, antall linjer og gjennomsnittet med én desimal.

b) Forklar i én setning hvorfor .split() ikke kan brukes på denne fila.

📝Oppgave 7
Eksamensnivå, sjanger O

Programmet skal skrive en ordbok til fil, én stasjon per linje, og lese den tilbake. Utskriften blir feil.

data = {'Kvitheia': 1180, 'Ospedalen': 1420}
with open('x3.txt', 'w') as outfile:
    for navn in data:
        outfile.write(navn + ';' + str(data[navn]))

with open('x3.txt') as infile:
    for line in infile:
        print(line.strip().split(';'))

a) Hva skriver programmet ut, og hva var meningen?

b) Rett feilen, og vis den nye utskriften.

API- og konstruksjonsliste

Begrepsbank

Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.

Skrivemodus `'w'`

Det andre argumentet til open. 'w' åpner fila for skriving og tømmer den umiddelbart; uten modusargument åpnes fila for lesing. Det finnes flere moduser i Python, men IN1900 trenger bare disse to — les og skriv. Åpner du en fil i 'w' og krasjer før du skriver noe, står du igjen med en tom fil.

`write` mot `print`
print legger til linjeskift og gjør tall om til tekst for deg. write gjør ingen av delene: du må skrive '\n' selv, og du må konvertere tall med str(...) eller en f-streng. Til gjengjeld skriver write nøyaktig det du ber om, noe som er poenget når fila skal leses av et program senere.
Symmetri mellom skriving og lesing

En fil du skriver, skal kunne leses tilbake av ditt eget innlesingsmønster. Skriver du en header-linje, må leseren hoppe over den med readline(); skriver du ingen, må den ikke. Velger du semikolon som separator, må leseren bruke .split(';'). Er oppgaveteksten uklar om header, velg selv og dokumenter valget i en kommentar.

Filtrering uten mutasjon

Å returnere en ny ordbok med de oppføringene som oppfyller et krav, og la argumentet være urørt. Mønsteret er ny = {} → løkke over .items()if-betingelse → innsetting → return ny utenfor løkka. Dette er del (c) av eksamensoppgaven, og formuleringen «uten å endre argumentet» står i oppgaveteksten fordi det er det som testes.

Mutasjon av argumentet

Å endre ordboka som ble sendt inn i funksjonen, i stedet for å bygge en ny. Fordi en ordbok sendes som en referanse til ett objekt, ser den som kalte funksjonen endringen umiddelbart. Det er feil #20, og det gjør funksjonen ubrukelig for den som skal filtrere samme datasett på flere grenser.

Aliasing

To navn på samme objekt. ny = data kopierer ikke ordboka — den gir bare et nytt navn, så endringer via ny skjer i data. Samme sak gjelder ett nivå ned: ny[k] = verdier gir en ny ytre ordbok, men de indre ordbøkene er felles. Feil #23. En helt uavhengig kopi krever at du bygger de indre ordbøkene på nytt, felt for felt.

Fast-bredde-fil (kolonnefil)

En tekstfil der feltene ikke skilles av et tegn, men står i faste tegnposisjoner, med mellomrom som utfylling. .split() er ubrukelig; feltene hentes med slicing line[a:b] og strippes. Formatet er vanlig i eldre måleserier. I IN1900 er dette bør kjenne til — nivå 3, få poeng.

Delvis uttelling på kolonnegrenser

Sensors uttalte praksis: å telle tegnposisjoner helt riktig uten fila foran seg er nesten umulig, så uttellingen ligger i metoden — slicing på faste posisjoner, .strip() på tekstfelt, konvertering av tallfelt, resten av mønsteret som før. Skriv de tallene du kommer fram til, dokumenter antakelsen i en kommentar, og gå videre.

Repetisjon: kapitlet i seks linjer

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.