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.
Dette kapitlet trener fil-deloppgaven i Oppgave 4, og bare den.
Filbehandling står i 5 av 6 moderne eksamenssett (83 %), nesten alltid som én
deloppgave inne i den store modelleringen, verdt 5–10 poeng. Sjangeren er
F — stor klassemodellering, altså å bygge samarbeidende klasser fra en
beskrevet oppgave.
Dette må sitte — bokas høyeste prioritetsnivå. Bestillingen er formulert
nesten likt hvert år: «les fila og opprett objektene». Det som varierer, er
dataformatet: skilletegnet, om det finnes en overskriftslinje, hvilke felt
som er tall, og om samlingen skal være en liste eller en ordbok.
Drillen roterer nettopp de fire tingene. Alt annet er den samme løkka du lærte i
kap. 7.1.
Kapitlet er langt (~75 min). Det er delt i tre økter med tidsanslag, og du kan trygt ta én økt om gangen.
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 19fil = 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 202. 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 AdaHar du de to, er resten rotasjon av dataformatet.
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 ekstrafil.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) ellersamling[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.
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.
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.
Innholdet i medlemsregister.txt:
navn;alder;timer
Ada;24;18
Nora;31;7
Per;19;25
Kari;42;3Skilletegnet 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 villeint("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.
(Innstegsoppgave — gjengivelse.) En fil varelager.txt har linjer på formenvarenavn,antall,pris, uten overskriftslinje:
skrue,120,4 mutter,80,2 bolt,45,7 skive,200,1a) Hvilket skilletegn skal .split() ha?
b) Hvilke av de tre feltene skal gjennom int(...)?
c) Skriv linjen som deler linja i felt.
Fila varelager.txt har linjer på formen varenavn,antall,pris uten overskriftslinje:
skrue,120,4 mutter,80,2 bolt,45,7 skive,200,1a) Skriv klassen Vare med konstruktør, aksessorer for antall og pris, en
metode verdi(self) som gir antall ganger pris, og en __str__ på formenskrue x120.
b) Skriv les_varer(filnavn) som returnerer en liste avVare-objekter.
c) Skriv kode som leser fila, skriver ut alle varene, og skriver ut den
samlede lagerverdien.
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 12a) 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?
Fila resultat.txt inneholder:
Ada:12:3
Nora:9:5
Per:15:1Hva 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)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;8a) 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.
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 klassenRegister som leser fila», er det denne; ber den om «en funksjon som returnerer en liste», er det den frittstående.
En resirkuleringsstasjon har fila containere.txt med linjer på formenmateriale;fylling:
glass;40 papir;85 metall;80 plast;12a) 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 metodenles_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?
Filamedlemsregister.txt har en overskriftslinje og linjer på formennavn;alder;timer:
navn;alder;timer
Ada;24;18
Nora;31;7
Per;19;25
Kari;42;3a) 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 ellerNone.
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.
En medstudent har skrevet denne metoden. Fila varelager.txt har linjer på formenvarenavn,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.
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
PerAda
Pera) Skriv klassen Spill med tittel, en tom liste av eiere, ogles_eiere(self) som bygger filnavnet av tittelen og fyller lista.
b) Skriv klassen Klubb som holder spillene i en ordbok fra tittel tilSpill-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?
Fila sykkelturer.txt inneholder:
R31;Blindern;12
R48;Majorstuen;5
R07;Toyen;23
R31;Blindern;8Hva 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)Fem feil koster nesten alle poengene som mistes på fil-deloppgaven:
- #8 — feil skilletegn. Les eksempellinjen i oppgaveteksten og bruk det
tegnet fila faktisk bruker. Feil tegn gir én stor bit og en IndexError.
- #8 — glemt .rstrip(). Linjeskiftet henger igjen i siste felt. Stum feil
som slår ut først når noen sammenligner.
- #4 — glemt int(...). Feltene er alltid tekster. Og motsatt: ikke
konverter et navn eller et postnummer.
- #1 — lokal samling i en metode. varer = {} i stedet for self._varer
gjør at alt forsvinner når metoden er ferdig, og objektet står tomt igjen.
- Rådata i stedet for objektreferanser. d[navn] = felt[1] lagrer en tekst.
Alle senere deloppgaver som skal spørre objektet om noe, faller da sammen.
Og to plasseringsfeil: samlingen opprettet inne i løkka (nullstilles hver runde), og glemt header-hopp (ValueError med overskriftsordet i feilmeldingen).
Begrepsbank
Begrepsbanken er flashcard- og repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.
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.
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.
d[felt[0]] = Vare(felt[0], int(felt[1]), int(felt[2])), ikked[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.
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».
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: ogd[k] = 0 før akkumuleringen.
Skal siste linje gjelde, er overskrivingen riktig. Les hva oppgaven ber om — begge deler forekommer.
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.
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.
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 duinvalid 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.
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.
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 besteBlokken 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.
É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.
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å.
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.
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.
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.