Tilbake
7.2

7.2 DRILL — Filinnlesing i klassemodellering

Full drill på filinnlesing som del av den store oppgaven: les en fil og bygg opp en samling av objektreferanser (liste/ordbok), med roterende dataformat.

75 min
10 oppgaver
DRILLFilinnlesing i klassemodellering
Din fremgang i kapitlet
0 / 10 oppgaver

Forkunnskaper — sist du var her

Kapitlet bygger på kap. 7.1 — Filinnlesing linje for linje
og kap. 6.2 — Flere samarbeidende klasser, og bruker klassene fra kap. 5.1 og
kap. 5.2.

De to tingene drillen hviler på, friskes opp med kjørt kode.

1. Filinnlesing-løkkas fem deler, her på fila medlemmer.txt:

Innholdet i medlemmer.txt:

Ada 24
Nora 31
Per 19

fil = open("medlemmer.txt")
linje = fil.readline()
while linje != "":
    felt = linje.rstrip().split()
    print(felt[0], int(felt[1]) + 1)
    linje = fil.readline()
fil.close()

Utskrift:

Ada 25
Nora 32
Per 20

2. En samling av objektreferanser, ikke av rådata:

class Medlem:
    def __init__(self, navn):
        self._navn = navn

    def hent_navn(self):
        return self._navn

alle = []
alle.append(Medlem("Ada"))
alle.append(Medlem("Nora"))
print(len(alle), alle[0].hent_navn())

Utskrift:

2 Ada

Har du de to, er resten rotasjon av dataformatet.

📜Løsningsoppskriften for fil-deloppgaven — åtte steg

Bruk den samme framgangsmåten hver gang, uansett hvordan fila ser ut.

Steg 0 — Les eksempellinjen i oppgaveteksten og skriv den av på kladden.
Marker skilletegnet, og skriv hvilket felt som er hva: felt[0] navn,
felt[1] alder, felt[2] timer. To minutter her sparer deg for begge halvdelene av feilkode #8.

Steg 1 — Opprett samlingen FØR løkka. [] hvis du bare skal gå gjennom alle,
{} hvis noe skal slås opp på et navn eller en kode senere.

Steg 2 — fil = open(filnavn).

Steg 3 — Hopp over headeren hvis fila har en, med ett ekstra
fil.readline().

Steg 4 — linje = fil.readline() og while linje != "":.

Steg 5 — Inne i løkka: felt = linje.rstrip().split(sep). Rens først, del
så, med det skilletegnet fila faktisk bruker.

Steg 6 — Lag objektet og legg det i samlingen. int(...) på hvert felt som
skal være tall, og bare på dem. samling.append(obj) eller
samling[felt[0]] = obj.

Steg 7 — linje = fil.readline() NEDERST i løkka, så fil.close() etter
den, og til slutt return samling etter løkka.

Åtte steg, og bare tre av dem endrer seg fra oppgave til oppgave: skilletegnet, headeren, og valget mellom liste og ordbok.

Fire ting som roterer

Det eneste som varierer mellom fil-deloppgavene i settene:

- Skilletegnet. Semikolon, komma, kolon eller mellomrom. Står i
eksempellinjen i oppgaveteksten. Mellomrom betyr .split() uten argument.
- Headeren. Har fila en overskriftslinje, hoppes den over med ett ekstra
fil.readline() før løkka.
- Hvilke felt som er tall. Bare de du skal regne eller sammenligne med som
tall, skal gjennom int(...). Navn, koder og postnumre forblir tekst.
- Liste eller ordbok. Skal noe slås opp på en nøkkel senere, er det en
ordbok. Ellers en liste.

Alt annet — de åtte stegene — er identisk hver gang.

Økt 1 — Den gjennomarbeidede casen (~20 min)

Under står en fil-deloppgave slik den ser ut på eksamen, med margnotater om hva som gir uttelling ved hvert steg.

✏️Eksempel 1: Full fil-deloppgave med sensor-margnotater

Et treningssenter har medlemmene sine i fila medlemsregister.txt. Fila har en overskriftslinje, og hver datalinje har formen navn;alder;timer.

Skriv klassen Medlem og en funksjon les_register(filnavn) som returnerer en
ordbok fra navn til Medlem-objekt. Skriv deretter en funksjon som finner
medlemmet med flest timer.

Steg 0 — les formatet. Fila ser slik ut:

Innholdet i medlemsregister.txt:

navn;alder;timer
Ada;24;18
Nora;31;7
Per;19;25
Kari;42;3

Skilletegnet er semikolon. Fila har en header. felt[0] er navn (tekst),
felt[1] er alder (tall), felt[2] er timer (tall). Samlingen skal være en
ordbok, fordi oppgaven sier «fra navn til objekt».

Steg 1–7 — hele løsningen:

class Medlem:
    def __init__(self, navn, alder, timer):
        self._navn = navn self._alder = alder self._timer = timer

    def hent_navn(self):
        return self._navn

    def hent_timer(self):
        return self._timer

    def __str__(self):
        return self._navn + " (" + str(self._alder) + " ar, " + str(self._timer) + " t)"

def les_register(filnavn):
    register = {}
    fil = open(filnavn)
    fil.readline()
    linje = fil.readline()
    while linje != "":
        felt = linje.rstrip().split(";")
        m = Medlem(felt[0], int(felt[1]), int(felt[2]))
        register[felt[0]] = m linje = fil.readline()
    fil.close()
    return register

def flest_timer(register):
    beste = None beste_timer = 0 for navn in register:
        m = register[navn]
        if m.hent_timer() > beste_timer:
            beste = m beste_timer = m.hent_timer()
    return beste

reg = les_register("medlemsregister.txt")
for navn in reg:
    print(reg[navn])
print("Antall:", len(reg))
print("Flest timer:", flest_timer(reg))

Utskrift:

Ada (24 ar, 18 t)
Nora (31 ar, 7 t)
Per (19 ar, 25 t)
Kari (42 ar, 3 t)
Antall: 4
Flest timer: Per (19 ar, 25 t)

Sensors margnotater, steg for steg.

register = {} før løkka (1 p). Inne i løkka ville den blitt nullstilt hver
runde, og bare det siste medlemmet ville overlevd.

fil.readline() alene (1 p) hopper over headeren. Uten den ville
int("alder") stoppet programmet med ValueError, og feilmeldingen ville inneholdt selve ordet fra overskriften.

linje.rstrip().split(";") (2 p). Rens før deling, og semikolon fordi det er
det fila bruker. Med .split(",") ville felt[1] gitt en IndexError.

int(felt[1]) og int(felt[2]), men ikke på felt[0] (2 p). Bare det du
skal regne med, konverteres.

register[felt[0]] = m (2 p). Nøkkelen er navnet, verdien er objektet
ikke rådataen. Lagrer du register[felt[0]] = felt[2], mister du tilgangen til metodene, og alle senere deloppgaver blir vanskeligere.

linje = fil.readline() nederst (2 p). Uten den henger programmet.

return register etter løkka (1 p). Inne i løkka ville funksjonen gitt en
ordbok med bare det første medlemmet — feilkode #7.

Finn-beste-løkka (4 p): beste = None og beste_timer = 0 før løkka,
sammenligningen, og at begge oppdateres sammen. Oppdaterer du bare beste, finner løkka det siste medlemmet over grensen i stedet for det største.

Riktig struktur belønnes selv med småfeil. Har du hele løkka riktig, men glemt
.rstrip(), er trekket lite — den delen av strukturen som er riktig, teller.

— naturlig pausepunkt —

Økt 2 — Roterende skilletegn og header (~28 min)

De fem første oppgavene varierer dataformatet: komma, kolon, mellomrom, med og uten overskriftslinje. Bruk de åtte stegene hver gang, og begynn alltid med steg
0 — å skrive av eksempellinjen og markere hva som er hva.

📝Oppgave 1

(Innstegsoppgave — gjengivelse.) En fil varelager.txt har linjer på formen
varenavn,antall,pris, uten overskriftslinje:

skrue,120,4 mutter,80,2 bolt,45,7 skive,200,1

a) Hvilket skilletegn skal .split() ha?

b) Hvilke av de tre feltene skal gjennom int(...)?

c) Skriv linjen som deler linja i felt.

📝Oppgave 2
Eksamensnivå, sjanger…

Fila varelager.txt har linjer på formen varenavn,antall,pris uten overskriftslinje:

skrue,120,4 mutter,80,2 bolt,45,7 skive,200,1

a) Skriv klassen Vare med konstruktør, aksessorer for antall og pris, en
metode verdi(self) som gir antall ganger pris, og en __str__ på formen
skrue x120.

b) Skriv les_varer(filnavn) som returnerer en liste av
Vare-objekter.

c) Skriv kode som leser fila, skriver ut alle varene, og skriver ut den
samlede lagerverdien.

📝Oppgave 3
Eksamensnivå, sjanger…

Fila timeplan.txt har en overskriftslinje og deretter linjer på formen dag rom antall, skilt med
mellomrom:

dag rom antall mandag A12 24 tirsdag B03 18 onsdag A12 31 torsdag C07 12

a) Skriv les_timeplan(filnavn) som returnerer en ordbok fra dag til
antall deltakere, og som hopper over headeren.

b) Skriv kode som skriver ut ordboka og det samlede antallet.

c) Hva ville skjedd om du hadde brukt .split(";") i stedet?

📝Oppgave 4
Eksamensnivå, sjanger A

Fila resultat.txt inneholder:

Ada:12:3
Nora:9:5
Per:15:1

Hva skriver programmet ut?

fil = open("resultat.txt")
linje = fil.readline()
beste = None beste_poeng = 0 antall = 0 while linje != "":
    felt = linje.rstrip().split(":")
    poeng = int(felt[1]) - int(felt[2])
    antall = antall + 1 if poeng > beste_poeng:
        beste = felt[0]
        beste_poeng = poeng linje = fil.readline()
fil.close()
print(antall, beste, beste_poeng)

📝Oppgave 5
Eksamensnivå, sjanger…

Fila sykkelturer.txt har linjer på formen kode;stativ;turer, uten overskriftslinje, og samme sykkelkode kan stå på flere linjer:

R31;Blindern;12
R48;Majorstuen;5
R07;Toyen;23
R31;Blindern;8

a) Skriv turer_per_sykkel(filnavn) som returnerer en ordbok fra sykkelkode
til samlet antall turer.

b) Skriv kode som skriver ut ordboka og antall ulike sykler.

c) Hva ville skjedd om du hadde skrevet d[felt[0]] = int(felt[2]) i stedet
for å akkumulere?

Økt 3 — Fila fyller en to-klasse-modell (~27 min)

De siste fem oppgavene er på full eksamensform: fila skal ikke bare bli en liste, den skal fylle en modell med to samarbeidende klasser. Det er nøyaktig det Oppgave 4 ber om.

Fil til to-klasse-modell

Formen der ytterklassen leser fila selv og fyller sin egen samling:

class Container:
    def __init__(self, navn):
        self._navn = navn self._elementer = {}

    def les_fra_fil(self, filnavn):
        fil = open(filnavn)
        linje = fil.readline()
        while linje != "":
            felt = linje.rstrip().split(sep)
            self._elementer[felt[0]] = Element(felt[0], int(felt[1]))
            linje = fil.readline()
        fil.close()

Blokken er en mal med plassholdernavn og er merket som ikke-kjørbar.

To ting skiller den fra en frittstående funksjon:

- Den legger objektene i self._elementer, ikke i en lokal liste, og
returnerer derfor ingenting.
- Den kan bygge filnavnet av objektets eget navn:
open(self._navn + ".txt").

Begge formene forekommer i settene. Ber oppgaven om «en metode i klassen
Register som leser fila», er det denne; ber den om «en funksjon som returnerer en liste», er det den frittstående.

📝Oppgave 6
Eksamensnivå, sjanger F

En resirkuleringsstasjon har fila containere.txt med linjer på formen
materiale;fylling:

glass;40 papir;85 metall;80 plast;12

a) Skriv klassen Container med konstruktør, aksessor for fyllingen og
__str__ på formen glass: 40 %.

b) Skriv klassen Stasjon med et sted, en tom liste, og metoden
les_fra_fil(self, filnavn) som fyller lista med Container-objekter.

c) Legg til skriv_alle(self) og antall_fulle(self), som teller
containere med fylling på minst 80.

d) Hvorfor heter metoden i c) skriv_alle og ikke skriv?

📝Oppgave 7
Eksamensnivå, sjanger F

Fila
medlemsregister.txt har en overskriftslinje og linjer på formen
navn;alder;timer:

navn;alder;timer
Ada;24;18
Nora;31;7
Per;19;25
Kari;42;3

a) Skriv klassen Medlem med konstruktør, aksessorer for navn og timer, og
__str__ på formen Ada/18.

b) Skriv klassen Senter med en ordbok fra navn til Medlem-objekt,
les_fra_fil(self, filnavn), og finn(self, navn) som gir medlemmet eller
None.

c) Legg til totalt_antall_timer(self).

d) Skriv kode som leser fila, slår opp «Per» og «Kari», slår opp en som ikke
finnes, og skriver ut totalen.

📝Oppgave 8
Eksamensnivå, sjanger I

En medstudent har skrevet denne metoden. Fila varelager.txt har linjer på formen
varenavn,antall,pris uten overskriftslinje.

class Lager:
    def __init__(self):
        self._varer = {}

    def les_fra_fil(self, filnavn):
        varer = {}
        fil = open(filnavn)
        linje = fil.readline()
        while linje != "":
            felt = linje.rstrip().split(";")
            varer[felt[0]] = felt[1]
            linje = fil.readline()
        fil.close()

    def antall_varer(self):
        return len(self._varer)

a) Metoden har tre feil. Hvilke?

b) Gi et konkret moteksempel: hva gir antall_varer() etter et kall på
les_fra_fil("varelager.txt"), og hva skulle det gitt?

c) Skriv den rettede metoden.

📝Oppgave 9
Eksamensnivå, sjanger F

En brettspillklubb har én fil per spill. Fila heter <spilltittel>.txt og har ett navn per linje.
Kolonier.txt og Ordjakt.txt inneholder:

Ada
Nora
Per

Ada
Per

a) Skriv klassen Spill med tittel, en tom liste av eiere, og
les_eiere(self) som bygger filnavnet av tittelen og fyller lista.

b) Skriv klassen Klubb som holder spillene i en ordbok fra tittel til
Spill-objekt, med legg_til(self, spill) og totalt_antall_eiere(self).

c) Lag begge spillene, les inn eierne, legg dem i klubben, og skriv ut
totalen.

d) Hvorfor kan ikke metoden i b) hete antall_eiere?

📝Oppgave 10
Eksamensnivå, sjanger A

Fila sykkelturer.txt inneholder:

R31;Blindern;12
R48;Majorstuen;5
R07;Toyen;23
R31;Blindern;8

Hva skriver programmet ut?

class Sykkel:
    def __init__(self, kode, turer):
        self._kode = kode self._turer = turer

    def hent_turer(self):
        return self._turer

    def __str__(self):
        return self._kode + "=" + str(self._turer)

d = {}
fil = open("sykkelturer.txt")
linje = fil.readline()
while linje != "":
    felt = linje.rstrip().split(";")
    d[felt[0]] = Sykkel(felt[0], int(felt[2]))
    linje = fil.readline()
fil.close()

for kode in d:
    print(d[kode])

total = 0 for kode in d:
    total = total + d[kode].hent_turer()
print(len(d), total)

API- og konstruksjonsliste

Begrepsbank

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

Steg 0 — les formatet først

Det viktigste steget, og det som oftest hoppes over.

Skriv av eksempellinjen fra oppgaveteksten på kladden, marker skilletegnet, og skriv hvilket felt som er hva: felt[0] navn, felt[1] alder, felt[2] timer.

To minutter der sparer deg for begge halvdelene av feilkode #8 — feil skilletegn og glemt rensing — og for å konvertere feil felt.

De fire tingene som roterer

Skilletegnet, headeren, hvilke felt som er tall, og valget mellom liste og ordbok.

Alt annet i de åtte stegene er identisk fra oppgave til oppgave. Kjenner du de fire variablene, er hver ny fil-deloppgave den samme oppgaven i ny innpakning.

Domenet varierer også — medlemsregister, varelager, timeplan, resultatliste — men det er bare kulisser.

Objektreferanser, ikke rådata
d[felt[0]] = Vare(felt[0], int(felt[1]), int(felt[2])), ikke
d[felt[0]] = felt[1].

Lagrer du rådataen, mister du tilgangen til objektets metoder, og alle senere deloppgaver — «finn den dyreste», «skriv ut alle», «regn ut samlet verdi» — blir vanskeligere eller umulige.

Det er en av de faste vurderingene i Oppgave 4, og det er en av grunnene til at fil-deloppgaven kommer først: resten bygger på den.

Liste eller ordbok

Ett spørsmål avgjør: skal noe slås opp på et navn eller en kode senere?

Er svaret ja, bruk en ordbok med navnet som nøkkel og objektet som verdi. Er svaret nei — du skal bare gå gjennom alle — er en liste enklere.

Oppgaveteksten sier det som regel rett ut: «returner en ordbok fra navn til objekt» eller «returner en liste av objekter».

Samme nøkkel på flere linjer

En ordbok kan aldri ha to like nøkler. Kommer samme navn på to linjer, blir det første objektet overskrevet uten en advarsel.

Skal verdiene legges sammen, må du bruke teller-idiomet: if k not in d: og
d[k] = 0 før akkumuleringen.

Skal siste linje gjelde, er overskrivingen riktig. Les hva oppgaven ber om — begge deler forekommer.

Innlesing som metode mot funksjon

Begge formene forekommer i settene.

Som metode: def les_fra_fil(self, filnavn): skriver til self._elementer
og returnerer ingenting. Ber oppgaven om «en metode i klassen Register som leser fila», er det denne.

Som funksjon: def les_alle(filnavn): bygger en lokal samling og returnerer
den. Ber oppgaven om «en funksjon som returnerer en liste», er det denne.

Blander du dem — en metode som bygger en lokal samling og ikke returnerer noe — forsvinner alt. Det er feilkode #1 i en metode.

Nøkkelen fra objektets egen aksessor
self._medlemmer[m.hent_navn()] = m.

Containeren bestemmer ikke navnet, den spør objektet. Alternativet m._navn er feilkode #11, direkte tilgang til en instansvariabel utenfra — og det er den vanligste formen den tar når to klasser samarbeider.

Formen er også tryggere: endrer Medlem hvordan navnet lagres, merker containeren ingenting.

Header i praksis

Ett fil.readline() alene, før løkka. Du trenger ikke å ta vare på verdien.

Glemmer du det, prøver programmet å konvertere et ord fra overskriften til tall, og stopper med ValueError — med selve ordet i feilmeldingen. Ser du
invalid literal for int() med et overskriftsord i, vet du med én gang hva som mangler.

Sjekk alltid om eksempelfila i oppgaveteksten har en overskriftslinje. Den er lett å overse fordi den ser ut som data.

Hvilke felt som skal konverteres

Bare de du faktisk skal regne eller sammenligne med som tall.

Navn, koder, romnumre og materialer forblir tekst. Postnummeret 0150 ville blitt tallet 150 og mistet nullen; en varekode som R07 lar seg ikke konvertere i det hele tatt.

Er du i tvil, spør: skal jeg noen gang legge sammen, trekke fra eller sammenligne denne verdien med >? Da er den et tall.

Finn-beste-løkka over en fil-samling

Mønsteret som ofte er den høyest vektede deloppgaven rett etter innlesingen:

    beste = None beste_verdi = 0 for e in samlingen:
        if e.hent_verdi() > beste_verdi:
            beste = e beste_verdi = e.hent_verdi()
    return beste

Blokken er en mal med plassholdernavn og er merket som ikke-kjørbar.

Begge variablene oppdateres sammen. Oppdaterer du bare beste, finner løkka
det siste elementet over grensen i stedet for det største. beste = None er det ærlige svaret på en tom samling.

Å dele oppgaven i to funksjoner

Én som leser fila, én som regner på resultatet.

Det er slik oppgaveteksten deler den også: «c) les fila og opprett objektene» og
«d) skriv en metode som finner …».

Gevinsten på eksamen er konkret: du kan få full uttelling på den ene selv om du står fast på den andre — og du kan bruke en metode oppgaven beskriver, uten selv å ha skrevet den.

Objektet leser sin egen fil
open(self._tittel + ".txt") inne i en metode på klassen.

Formen er en gjenganger i settene: hvert objekt har sin egen fil, navngitt etter objektet. Punktumet og filendelsen skrives inn selv.

Metoden returnerer som regel ingenting — den fyller objektets egen samling, og resultatet hentes med en aksessor etterpå.

Delvis uttelling i fil-deloppgaven

Poengene ligger spredt utover, og hver del teller for seg: samlingen før løkka,
open, header-hoppet, løkkas fem deler, rensingen, delingen, konverteringene, opprettelsen av objektet, innsettingen, og return etter løkka.

Det betyr at en glemt .rstrip() koster lite når resten står. Og det betyr at det aldri lønner seg å stå fast: skriv løkka ferdig med det du er sikker på, og gå videre.

Er du usikker på et format, skriv en kommentar om hva du har forutsatt. Det er den ene legitime bruken av kommentarer i en besvarelse.

Å teste uten å kjøre

Eksamen er hjelpemiddelfri, så koden din blir aldri kjørt før den rettes.
Kontrollen må derfor gjøres med øynene.

Tre spørsmål tar de vanligste feilene: Står readline to steder? Står samlingen over løkka og return under den? Stemmer skilletegnet med eksempellinjen?

Bruk et halvt minutt på de tre etter hver fil-deloppgave. Det er den billigste kontrollen som finnes.

Repetisjon — de sju setningene som må sitte

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.