Tilbake
7.7

7.7 DRILL — Ledd 4–5: utskrift/lagring og meny/`main` som binder kjeden

Det siste leddet og limet: pen formatert utskrift eller lagring til fil, og en main/meny-løkke som kaller hele kjeden og gjentar til brukeren avslutter.

60 min
10 oppgaver
DRILLLedd 4–5utskrift/lagringmeny/`main` som binder kjeden
Din fremgang i kapitlet
0 / 10 oppgaver
Forkunnskaper — sist du var her

Verktøyet er gjennomgått i kap. 3.4 (formatert utskrift), kap. 4.2 (filskriving og binærlagring) og kap. 2.2 (meny og hovedfunksjon). Her er de tre grepene, ferdig oppfrisket.

1. Fast kolonnebredde med ljust og rjust:

print('sykkel'.ljust(14) + '450'.rjust(6))
print('kjokkenbord'.ljust(14) + '900'.rjust(6))

Utskrift:

sykkel           450
kjokkenbord      900

2. Skriv til fil — husk linjeskiftet:

f = open('test.txt', 'w')
f.write('B1;530\n')
f.close()
f = open('test.txt', 'r')
print(f.read())
f.close()

Utskrift:

B1;530

3. Menyløkka som terminerer (kap. 2.2):

while True:
    valg = input('Valg: ')
    if valg == 'q':
        break

Blokken over er mønsteret. Den kan ikke kjøres her, fordi den venter på tastatur.

I tillegg forutsetter kapitlet kap. 7.6, som ga oss de robuste analysefunksjonene menyen skal kalle.

Løkke 1 — pen utskrift i kolonner (~13 min)

Første halvdel av ledd 5: å få tallene til å stå under hverandre. Det er ren håndverkskunnskap, og det er raskt lært.

Utskriften er en egen deloppgave

Utskriftsfunksjonen tar ferdige data inn og skriver dem ut. Den regner ikke selv, og den leser ikke fil.

def skriv_oversikt(tabell):
    d = omsetning_per_bod(tabell)
    ...

Blokken over er et utdrag av en funksjon som står ferdig i eksempel 1.

Kaller den en analysefunksjon for å få tak i dataene, er det gjenbruk og gir uttelling. Gjentar den analysen med en egen løkke, er det kopiert kode og gir mindre.

Utskriftsfunksjonen returnerer som regel ingenting — den skriver. Det er det motsatte av analysefunksjonene, som returnerer og aldri skriver. Å blande de to rollene er en av de vanligste strukturfeilene i faget.

`ljust(n)`, `rjust(n)` og `center(n)`

Fyller en streng ut til bredden n med mellomrom: til venstre, til høyre eller på begge sider.

print('bod'.ljust(8) + '|')
print('bod'.rjust(8) + '|')
print('bod'.center(8) + '|')

Utskrift:

bod     |
     bod|
  bod   |

Tommelfingerregelen for en tabell: tekst venstrejusteres, tall høyrejusteres. Da står sifrene under hverandre, og tallene er lette å sammenligne med øyet.

To ting å huske: metodene virker bare på strenger, så tall må gjennom str(...) først. Og er teksten lengre enn n, gjør de ingenting — de kutter aldri.

`'{:10.2f}'.format(x)` og f-strengen

Formatoppskriften gjør tre ting i ett: setter bredden, setter antall desimaler, og høyrejusterer tallet.

print('{:10.2f}'.format(530.0) + '|')
print('{:10.2f}'.format(1250.5) + '|')
print(f'{530.0:10.2f}' + '|')

Utskrift:

    530.00|
   1250.50|
    530.00|

10 er den totale bredden, .2f er to desimaler. Tallet høyrejusteres automatisk, så du trenger ikke rjust i tillegg.

De to formene er sidestilt. .format er den arkivet 2014–2019 bruker; f-strengen er nyere (verifiser mot gjeldende pensum — varslet inn 2019). Denne boka viser begge, og begge gir full pott.

For heltall bruker du {:6d} eller str(x).rjust(6) — begge deler virker.

Overskriftslinje og skillelinje

To linjer som gjør en utskrift til en tabell, og som koster nesten ingenting:

print('Bod'.ljust(6) + 'Omsetning'.rjust(10))
print('-' * 16)
print('B1'.ljust(6) + '{:10.2f}'.format(530.0))

Utskrift:

Bod    Omsetning
----------------
B1        530.00

'-' * 16 gjentar bindestreken 16 ganger — det er * på strenger fra kap. 3.3. Bredden skal være summen av kolonnebreddene, her 6 + 10.

Overskriftslinja skal bruke de samme breddene som datalinjene, ellers står den skjevt. Det er lett å sjekke: skriv ut begge og se om Omsetning slutter der tallene slutter.

Er dette verdt poeng? Ja — oppgavetekstene ber ofte om «en oversiktlig utskrift», og en tabell med overskrift er nettopp det.

✏️Eksempel 1: omsetningen per bod på loppemarkedet
«Filen lopper.txt har overskriftsraden bod;vare;pris;solgt. Skriv omsetning_per_bod(tabell) som returnerer {bod: samlet omsetning}, og skriv_oversikt(tabell) som skriver ut en pen tabell med bod og omsetning.»

Slik ser fila ut:

Innholdet i lopper.txt:

bod;vare;pris;solgt
B1;sykkel;450;1
B2;bokkasse;120;3
B1;lampe;80;2
B3;kjokkenbord;900;1
B2;platesamling;250;1

Innlesingen fra kap. 7.2:

def les_salg(filnavn):
    f = open(filnavn, 'r')
    overskrift = f.readline().strip().split(';')
    k_pris = overskrift.index('pris')
    k_solgt = overskrift.index('solgt')
    tabell = [overskrift]
    for linje in f:
        felt = linje.strip().split(';')
        felt[k_pris] = int(felt[k_pris])
        felt[k_solgt] = int(felt[k_solgt])
        tabell.append(felt)
    f.close()
    return tabell

Analysen fra kap. 7.4 — omsetningen er pris ganger antall solgte:

def omsetning_per_bod(tabell):
    k_bod = tabell[0].index('bod')
    k_pris = tabell[0].index('pris')
    k_solgt = tabell[0].index('solgt')
    d = {}
    for rad in tabell[1:]:
        belop = rad[k_pris] * rad[k_solgt]
        d[rad[k_bod]] = d.get(rad[k_bod], 0) + belop
    return d

Utskriften, som kaller analysen:

def skriv_oversikt(tabell):
    d = omsetning_per_bod(tabell)
    print('Bod'.ljust(6) + 'Omsetning'.rjust(10))
    print('-' * 16)
    for bod in sorted(d):
        print(bod.ljust(6) + '{:10.2f}'.format(d[bod]))

Utskrift: ingen — blokken definerer bare funksjonen. Den skriver først når den blir kalt med en ferdig tabell.

Kjører du skriv_oversikt(les_salg('lopper.txt')) etter at de tre funksjonene over er definert, skriver programmet:

Utskrift:

Bod    Omsetning
----------------
B1        610.00
B2        610.00
B3        900.00

B1 har solgt sykkel for 450 og to lamper for 80, altså 610 kroner.

Slik ville sensor sett på det. Utskriftsdeloppgaven er typisk 10 %, og fire ting gir uttelling hver for seg: at funksjonen kaller analysen i stedet for å regne selv, overskriftslinja, fast kolonnebredde, og sorted(d) som gir en forutsigbar rekkefølge.

Flere korrekte løsninger. print(bod.ljust(6), '{:10.2f}'.format(d[bod])) med komma i stedet for + gir nesten det samme — bare med ett ekstra mellomrom mellom kolonnene, fordi print selv skiller argumentene med mellomrom. Begge gir full pott; du må bare være konsekvent, ellers står overskriften skjevt.

Kantfeilen: '{:10.2f}'.format(...) på en streng gir ValueError: Unknown format code 'f' for object of type 'str'. Er du usikker på om kolonnen er tall, konverter først — eller bruk str(x).rjust(10), som virker på alt.

📝Oppgave 1

(Innstegsoppgave, sjanger B — kodesporing, altså «hva skriver dette programmet ut?».) Hva skriver programmet ut? Tegn opp linjene nøyaktig, med mellomrommene.

print('B1'.ljust(6) + '610'.rjust(8) + '|')
print('B22'.ljust(6) + '9000'.rjust(8) + '|')
print('B333333333'.ljust(6) + '5'.rjust(8) + '|')

📝Oppgave 2
Eksamensnivå, sjanger I
«Skriv skriv_varelinjer(tabell) som skriver ut én linje per vare med varenavn venstrejustert i 14 tegn, prisen høyrejustert i 6, antall solgte høyrejustert i 4, og samlet beløp med to desimaler i 10 tegn. Legg på en overskriftslinje og en skillelinje.»

Løkke 2 — oppramsing og rapport til fil (~11 min)

Andre halvdel av ledd 5: å få resultatet ut av programmet og inn i en fil noen andre kan lese.

`', '.join(liste)` — oppramsingen

Limer sammen elementene i ei liste med en skilletekst mellom. Skilleteksten står foran punktumet, lista i parentesen.

varer = ['sykkel', 'lampe', 'bokkasse']
print(', '.join(varer))
print(' | '.join(varer))
print(';'.join(varer))

Utskrift:

sykkel, lampe, bokkasse
sykkel | lampe | bokkasse
sykkel;lampe;bokkasse

Den er den naturlige partneren til split fra kap. 3.3: split deler en linje i felt, join setter feltene sammen til en linje igjen. Det er nettopp det du trenger når du skal skrive en rad til fil.

Kravet: alle elementene må være strenger. Har du tall i lista, får du TypeError: sequence item 0: expected str instance, int found. Konverter først:

rad = ['B1', 610]
print(';'.join([str(x) for x in rad]))

Utskrift:

B1;610

Skriv til fil: `write` og linjeskiftet
f.write(tekst) skriver teksten uten å legge på linjeskift. Det må du gjøre selv.

f = open('demo.txt', 'w')
f.write('B1;610')
f.write('B2;610\n')
f.write('B3;900\n')
f.close()
f = open('demo.txt', 'r')
print(f.read())
f.close()

Utskrift:

B1;610B2;610
B3;900

Den første og den andre linja rant sammen, fordi den første manglet '\n'. Det er den vanligste feilen i sjanger I, og den er stum — programmet skriver fint, fila blir bare ubrukelig for den som skal lese den etterpå.

Modusen 'w' tømmer fila før den skriver. Skal du legge til på slutten i stedet, bruker du 'a' — se kap. 4.2.

Og: hver open skal ha en close. Ved skriving er det viktigere enn ved lesing, fordi data kan bli liggende i minnet og aldri nå fila.

✏️Eksempel 2: rapporten skrevet til fil
«Skriv lagre_rapport(tabell, filnavn) som skriver omsetningen per bod til en tekstfil med overskriftsrad og semikolon som skilletegn. Funksjonen skal si fra til brukeren når rapporten er lagret.»
def lagre_rapport(tabell, filnavn):
    d = omsetning_per_bod(tabell)
    f = open(filnavn, 'w')
    f.write('bod;omsetning\n')
    for bod in sorted(d):
        f.write(bod + ';' + str(d[bod]) + '\n')
    f.close()
    print('Rapport lagret i', filnavn)

Utskrift: ingen — blokken definerer bare funksjonen. Den skriver først når den blir kalt.

Kjører du lagre_rapport(les_salg('lopper.txt'), 'rapport.txt') etter at les_salg og omsetning_per_bod er definert, skriver programmet:

Utskrift:

Rapport lagret i rapport.txt

Og fila som ble laget:

Utskrift:

Rapport lagret i rapport.txt

bod;omsetning
B1;610
B2;610
B3;900

Legg merke til at rapporten har samme format som inndatafila: overskriftsrad, semikolon, én rad per linje. Det er ikke tilfeldig — da kan rapporten leses inn igjen med den samme innlesingsfunksjonen, og det er nettopp den slags gjenbruk oppgavetekstene noen ganger ber om i den siste deloppgaven.

Slik ville sensor sett på det: fire ting gir uttelling — filnavnet som parameter, '\n' på hver linje, close() etter løkka, og beskjeden til brukeren. Den siste glemmes ofte, og den er nevnt eksplisitt i oppgavetekstene: brukeren skal få vite at det gikk bra.

Kantfeilen: f.write(d[bod]) uten str(...) gir TypeError: write() argument must be str, not int. Denne krasjer, og er derfor snillere enn det glemte linjeskiftet.

📝Oppgave 3
Eksamensnivå, sjanger I
«Skriv lagre_varer(tabell, filnavn) som skriver hele varetabellen til fil, med overskriftsraden først og semikolon mellom feltene. Bruk join, ikke en lang +-kjede.»
📝Oppgave 4
Eksamensnivå, sjanger B

Hva står i fila ut.txt når programmet er ferdig? Skriv innholdet nøyaktig, med linjeskift der de er.

f = open('ut.txt', 'w')
f.write('a')
f.write('b\n')
f.write('c')
f.close()
f = open('ut.txt', 'r')
print(f.read())
f.close()

Løkke 3 — lagring på binærfil (~11 min)

Noen sett ber om at en ordbok eller en tabell lagres slik at den kan hentes tilbake som den var, med tall som tall. Da er tekstfil upraktisk, og pickle er svaret.

`pickle` — lagre en struktur som den er
pickle lagrer en hel Python-struktur — ordbok, liste, 2D-tabell — til en binærfil, og henter den tilbake med typene i behold.

Fire ting du må ha på plass:

- import pickle øverst i fila.
- open(filnavn, 'wb') for skriving og open(filnavn, 'rb') for lesing. b står for binær; 'w' og 'r' alene gir TypeError.
- pickle.dump(objekt, f) skriver, pickle.load(f) leser.
- Fila er ikke lesbar tekst. Åpner du den i en teksteditor, ser du bare rot — og den skal derfor aldri vises i en oppgave.

Forskjellen fra en tekstfil i én setning: tekstfila må parses tilbake, binærfila kommer ferdig.

Når tekstfil, og når binærfil

Oppgaveteksten sier det som regel. Er den taus, avgjør formålet:

FormåletFormat
et menneske skal lese resultatettekstfil
et annet program skal lese dettekstfil, som regel
ditt eget program skal hente det tilbake senerebinærfil med pickle
strukturen er nøstet (ordbok av lister)binærfil — den slipper parsingen

pickle er merket «bør kjenne til» i denne boka: den står i omtrent 30 % av settene, og alltid som en liten deloppgave. Lær de fire linjene, ikke mer.

✏️Eksempel 3: omsetningsordboka lagret og hentet tilbake
«Skriv lagre_binaer(d, filnavn) som lagrer omsetningsordboka på binærfil, og hent_binaer(filnavn) som henter den tilbake. Vis at det som kommer ut, er identisk med det som gikk inn.»
import pickle

def lagre_binaer(d, filnavn):
    f = open(filnavn, 'wb')
    pickle.dump(d, f)
    f.close()

def hent_binaer(filnavn):
    f = open(filnavn, 'rb')
    d = pickle.load(f)
    f.close()
    return d

inn = {'B1': 610, 'B2': 610, 'B3': 900}
lagre_binaer(inn, 'omsetning.pkl')
ut = hent_binaer('omsetning.pkl')
print(ut)
print(ut == inn)
print(type(ut['B1']))

Utskrift:

{'B1': 610, 'B2': 610, 'B3': 900}
True
<class 'int'>

True er hele poenget. Ordboka kom tilbake med de samme nøklene, de samme verdiene og de samme typene — 610 er fortsatt et heltall, ikke strengen '610'.

Innholdet i omsetning.pkl vises ikke her, og skal ikke vises: fila er binær og ikke lesbar tekst.

Sammenlign med tekstfil-varianten:

inn = {'B1': 610, 'B2': 610}
f = open('omsetning.txt', 'w')
for bod in sorted(inn):
    f.write(bod + ';' + str(inn[bod]) + '\n')
f.close()

ut = {}
f = open('omsetning.txt', 'r')
for linje in f:
    felt = linje.strip().split(';')
    ut[felt[0]] = int(felt[1])
f.close()
print(ut == inn)

Utskrift:

True

Samme resultat — men veien tilbake krevde fem linjer med parsing, og du måtte huske int(). Det er nettopp den jobben pickle fjerner.

Slik ville sensor sett på det: deloppgaven er som regel liten, og de fire tingene som gir uttelling er import pickle, 'wb'/'rb', dump/load i riktig retning, og close(). Bytter du om 'w' og 'wb', krasjer det med TypeError: write() argument must be str, not bytes — så feilen er i det minste synlig.

Kantfeilen: pickle.load på en fil som ikke finnes gir FileNotFoundError, akkurat som vanlig lesing. Skal programmet takle at rapporten ikke er lagret ennå, hører den i try/except — se løkke 5.

📝Oppgave 5
Eksamensnivå, sjanger I
«Skriv lagre_tabell(tabell, filnavn) og hent_tabell(filnavn) som lagrer og henter hele 2D-tabellen på binærfil. Vis med én utskrift at tallkolonnene fortsatt er tall etter at tabellen er hentet tilbake.»
📝Oppgave 6
Eksamensnivå

For hver av disse fire bestillingene: skal resultatet lagres som tekstfil eller som binærfil? Svar med én setning hver.

a) «Lagre en rapport som styret i borettslaget skal skrive ut.»

b) «Lagre den nøstede ordboka {bod: {vare: antall}} slik at programmet kan hente den fram neste gang det kjøres.»

c) «Lagre resultatene slik at de kan leses inn igjen av innlesingsfunksjonen fra deloppgave a).»

d) «Lagre en logg der hver ny kjøring legger til linjer på slutten.»

Løkke 4 — menyen (~13 min)

Ledd 4: å la brukeren velge hva som skal skje. Mønsteret er kort, men det har fem detaljer som hver for seg er testet på eksamen.

Menyløkka `while True:` med `break`

Løkka skal gå til brukeren selv sier stopp, ikke et fast antall ganger. Derfor while True:, og derfor må det finnes nøyaktig én vei ut: break i avslutningsgrenen.

while True:
    valg = input('Valg: ')
    if valg == 'q':
        break

Blokken over er mønsteret. Den kan ikke kjøres her, fordi den venter på tastatur.

Glemmer du break, går programmet for alltid, og brukeren må avbryte det. Det er felle #8 — en input-løkke som ikke terminerer riktig, og den er verdt hele deloppgaven.

Menyteksten skal stå inne i løkka, så brukeren ser valgene på nytt hver runde. Står den utenfor, vises den én gang, og resten av økta må brukeren huske hva tallene betydde.

Menyvalget er en streng
input() gir alltid tekst, også når brukeren skriver et tall. Testen er derfor valg == '1' med fnutter.

Skriver du valg == 1, sammenligner du tekst med tall. Det gir ingen feilmelding — det gir bare False, hver eneste gang, og programmet havner rett i else-grenen uansett hva brukeren taster.

valg = '1'
print(valg == '1')
print(valg == 1)

Utskrift:

True
False

Det er felle #1, glemt konvertering, i sin mest stillegående form. Alternativet er å konvertere med int(valg) — men da krasjer programmet hvis brukeren skriver q. Hold valget som tekst.

Dispatcher: `if`/`elif`/`else`

Kjeden som fordeler jobben videre til de andre funksjonene. Hver gren gjør ett kall, ingen regning.

if valg == '1':
    skriv_oversikt(tabell)
elif valg == '2':
    lagre_rapport(tabell, 'rapport.txt')
elif valg == 'q':
    break
else:
    print('Ugyldig valg, prøv igjen.')

Blokken over er mønsteret; den ferdige funksjonen står i eksempel 4.

To grunner til at grenene skal være ettlinjes kall: du får gjenbrukspoengene, og menyen blir lesbar som en innholdsfortegnelse over programmet.

else-grenen er ikke pynt. Uten den tier programmet ved en skrivefeil, og brukeren tror valget ble utført. Den er nevnt i oppgavetekster, og den koster to linjer.

✏️Eksempel 4: menyen på loppemarkedet
«Skriv meny(tabell) som viser tre valg — vis omsetning per bod, lagre rapport til fil, og avslutt — og kaller de tidligere funksjonene til brukeren skriver q

Funksjonen selv inneholder input(), så blokken under definerer bare. Den kan ikke kjøres uten en bruker ved tastaturet:

def meny(tabell):
    while True:
        print()
        print('1 - vis omsetning per bod')
        print('2 - lagre rapport til fil')
        print('q - avslutt')
        valg = input('Valg: ')
        if valg == '1':
            skriv_oversikt(tabell)
        elif valg == '2':
            lagre_rapport(tabell, 'rapport.txt')
        elif valg == 'q':
            print('Ha det.')
            break
        else:
            print('Ugyldig valg, prøv igjen.')

Utskrift: ingen — blokken definerer bare funksjonen. Den venter på tastatur når den blir kalt.

Kjøringen under er ekte. Brukeren valgte 1, deretter 2, deretter en skrivefeil, og til slutt q. Pilene står ikke på skjermen — de er lagt til her for å vise hva brukeren tastet inn.

Kjøring:


1 - vis omsetning per bod
2 - lagre rapport til fil
q - avslutt
Valg: 1        <- skrevet av brukeren
Bod    Omsetning
----------------
B1        610.00
B2        610.00
B3        900.00

1 - vis omsetning per bod
2 - lagre rapport til fil
q - avslutt
Valg: 2        <- skrevet av brukeren
Rapport lagret i rapport.txt

1 - vis omsetning per bod
2 - lagre rapport til fil
q - avslutt
Valg: 5        <- skrevet av brukeren
Ugyldig valg, prøv igjen.

1 - vis omsetning per bod
2 - lagre rapport til fil
q - avslutt
Valg: q        <- skrevet av brukeren
Ha det.

Slik ville sensor sett på det. Fem ting gir uttelling hver for seg:

GrepHva som skjer uten det
while True:menyen kjøres én gang og programmet slutter
break i q-grenenprogrammet går for alltid (felle #8)
valg == '1' med fnutteringen gren treffer noen gang (felle #1)
menyteksten inne i løkkavalgene vises bare første runde
else-grenenskrivefeil får ingen tilbakemelding

Legg merke til at hver gren er ett kall. meny regner ingenting selv — den fordeler. Det er den samme delegeringsregelen som for main i kap. 2.2, og det er nettopp den som gjør gjenbruket synlig for sensor.
Kantfeilen: hadde q-grenen stått etter else, ville den aldri blitt nådd — else fanger alt som ikke traff en tidligere gren. Rekkefølgen i en if/elif-kjede er en del av logikken.

📝Oppgave 7
Eksamensnivå, sjanger H
«Skriv enkel_meny(tabell) med to valg: 1 skriver ut antall varer i tabellen, og q avslutter. Alt annet skal gi en beskjed om at valget er ugyldig.»

Vis funksjonen, og forklar med én setning hvorfor den ikke kan kjøres i en kodeblokk her i boka.

📝Oppgave 8
Eksamensnivå, sjanger D

I denne menyen mangler én linje, markert #KODE1:

def meny(tabell):
    while True:
        valg = input('Velg 1 eller q: ')
        if valg == '1':
            print(len(tabell))
        elif valg == 'q':
            #KODE1
        else:
            print('Ugyldig.')

Blokken over er et fragment med en manglende linje, ikke et kjørbart program.

Hvilket alternativ hører hjemme på #KODE1?

a) return tabell
b) break
c) continue
d) valg = 'q'

Løkke 5 — main binder kjeden (~12 min)

Siste ledd: funksjonen som eier programmet. Den leser fila én gang, sier fra hvis noe gikk galt, og gir stafettpinnen videre.

`main` leser fila én gang

Hovedfunksjonen skaffer dataene og sender den samme tabellen videre til alle de andre funksjonene.

def main(filnavn):
    tabell = les_salg(filnavn)
    meny(tabell)

Blokken over er mønsteret; den ferdige funksjonen står i eksempel 5.

Motstykket — at hver menygren leser fila på nytt — koster deg tre ting: filnavnet må hardkodes flere steder, funksjonene blir ubrukelige på andre data, og programmet gjør den samme jobben om og om igjen.

main gjør ingen regning selv. Kjennetegnet på en god main er at den er så kort at du ser hele programmets gang samtidig på skjermen.

Og: kall den direkte med main('lopper.txt'). Boka bruker aldri vaktsetningen med __name__, fordi den ikke gir uttelling på eksamen.

Tilbakemelding om suksess og feil

Brukeren skal vite hva som skjedde. Tre steder er det verdt en print:

- Etter innlesingen: «Leste 5 salgslinjer.» Da ser brukeren at fila ble funnet og hvor mye som kom inn.
- Etter lagring: «Rapport lagret i rapport.txt.» Uten den er skjermen taus, og brukeren vet ikke om det gikk bra.
- Ved feil: «Fant ikke fila lopper.txt.» Bedre enn en Traceback brukeren ikke forstår.

Dette er nevnt i oppgavetekster, og det koster én linje hver. Det er også god programmering utenfor eksamen — et program som gjør noe uten å si fra, er et program ingen stoler på.

`try/except` rundt fil-I/O i hovedfunksjonen

Manglende fil er en feil som kommer utenfra, og den hører derfor i try/except — ikke i en if-test.

try:
    tabell = les_salg(filnavn)
except FileNotFoundError:
    print('Fant ikke fila', filnavn)
    return

Blokken over er et utdrag av main; hele funksjonen står i eksempel 5.

Tre valg som er verdt poeng:

- Snever try: bare open-kallet, ikke hele programmet. Da fanger du det du mente å fange.
- Spesifikk except: FileNotFoundError, ikke naken except:. En naken except fanger også dine egne skrivefeil.
- return etter feilmeldingen: uten den fortsetter main med en tabell som ikke finnes, og krasjer likevel — bare litt senere.

Se kap. 4.3 for hele unntaksapparatet.

✏️Eksempel 5: hele loppemarkedsappen

Sett sammen alle fem leddene til én applikasjon: les fila, regn ut omsetningen, vis den, lagre den, og la brukeren styre med en meny. Hver funksjon i sin egen blokk, som deloppgavene på et sett.

Deloppgave a) — innlesing (ledd 1 og 2):

def les_salg(filnavn):
    f = open(filnavn, 'r')
    overskrift = f.readline().strip().split(';')
    k_pris = overskrift.index('pris')
    k_solgt = overskrift.index('solgt')
    tabell = [overskrift]
    for linje in f:
        felt = linje.strip().split(';')
        felt[k_pris] = int(felt[k_pris])
        felt[k_solgt] = int(felt[k_solgt])
        tabell.append(felt)
    f.close()
    return tabell

Deloppgave b) — analysen (ledd 3). Den tar tabellen som parameter og leser aldri fil:

def omsetning_per_bod(tabell):
    k_bod = tabell[0].index('bod')
    k_pris = tabell[0].index('pris')
    k_solgt = tabell[0].index('solgt')
    d = {}
    for rad in tabell[1:]:
        belop = rad[k_pris] * rad[k_solgt]
        d[rad[k_bod]] = d.get(rad[k_bod], 0) + belop
    return d

Deloppgave c) — utskriften (ledd 5). Den kaller b):

def skriv_oversikt(tabell):
    d = omsetning_per_bod(tabell)
    print('Bod'.ljust(6) + 'Omsetning'.rjust(10))
    print('-' * 16)
    for bod in sorted(d):
        print(bod.ljust(6) + '{:10.2f}'.format(d[bod]))

Utskrift: ingen — blokken definerer bare funksjonen. Den skriver når meny kaller den.

Deloppgave d) — lagringen (ledd 5). Den kaller også b):

def lagre_rapport(tabell, filnavn):
    d = omsetning_per_bod(tabell)
    f = open(filnavn, 'w')
    f.write('bod;omsetning\n')
    for bod in sorted(d):
        f.write(bod + ';' + str(d[bod]) + '\n')
    f.close()
    print('Rapport lagret i', filnavn)

Utskrift: ingen — blokken definerer bare funksjonen. Den skriver når meny kaller den.

Deloppgave e) — menyen (ledd 4). Hver gren er ett kall:

def meny(tabell):
    while True:
        print()
        print('1 - vis omsetning per bod')
        print('2 - lagre rapport til fil')
        print('q - avslutt')
        valg = input('Valg: ')
        if valg == '1':
            skriv_oversikt(tabell)
        elif valg == '2':
            lagre_rapport(tabell, 'rapport.txt')
        elif valg == 'q':
            print('Ha det.')
            break
        else:
            print('Ugyldig valg, prøv igjen.')

Utskrift: ingen — blokken definerer bare funksjonen. Den venter på tastatur når den blir kalt.

Deloppgave f) — main (limet). Fem linjer, og fila leses ett eneste sted:

def main(filnavn):
    try:
        tabell = les_salg(filnavn)
    except FileNotFoundError:
        print('Fant ikke fila', filnavn)
        return
    print('Leste', len(tabell) - 1, 'salgslinjer.')
    meny(tabell)

Utskrift: ingen — blokken definerer bare funksjonen. Den kjører når du kaller main(...).

Hele appen kjørt. Brukeren ser omsetningen, lagrer rapporten og avslutter:

Kjøring:

Leste 5 salgslinjer.

1 - vis omsetning per bod
2 - lagre rapport til fil
q - avslutt
Valg: 1        <- skrevet av brukeren
Bod    Omsetning
----------------
B1        610.00
B2        610.00
B3        900.00

1 - vis omsetning per bod
2 - lagre rapport til fil
q - avslutt
Valg: 2        <- skrevet av brukeren
Rapport lagret i rapport.txt

1 - vis omsetning per bod
2 - lagre rapport til fil
q - avslutt
Valg: q        <- skrevet av brukeren
Ha det.

Pilene står ikke på skjermen; de viser hva brukeren tastet.

Og hva som skjer når fila mangler:

def les_salg(filnavn):
    f = open(filnavn, 'r')
    f.close()
    return []

def main(filnavn):
    try:
        tabell = les_salg(filnavn)
    except FileNotFoundError:
        print('Fant ikke fila', filnavn)
        return
    print('Leste', len(tabell), 'linjer.')

main('finnes-ikke.txt')

Utskrift:

Fant ikke fila finnes-ikke.txt

En setning brukeren forstår, i stedet for fem linjer Traceback.

Slik ville sensor sett på det, deloppgave for deloppgave:

DeloppgaveVektHva som gir uttelling
a) innlesing15 %strip().split(';'), konvertering, kolonner ved navn
b) analysen20 %telleordboka, pris * solgt, tabellen som parameter
c) utskrift15 %kaller b), fast kolonnebredde, overskriftslinje
d) lagring15 %filnavn som parameter, '\n', close, beskjed til brukeren
e) meny20 %while True:, break, valg som tekst, else-gren
f) main15 %leser fila én gang, try/except, return etter feil

Gjenbruket, sagt rett ut: skriv_oversikt og lagre_rapport kaller begge omsetning_per_bod. meny kaller begge to. main kaller les_salg og meny. Ingen funksjon gjentar en annens arbeid, og fila åpnes ett sted. Det er den strukturen sensor leter etter, og den er verdt mer enn perfeksjon i noen enkelt deloppgave.
📝Oppgave 9
Eksamensnivå, sjanger H
«Skriv hovedprogram(filnavn) som leser tabellen, skriver ut hvor mange varer som ble lest, og deretter viser menyen. Hvis fila ikke finnes, skal programmet si fra og avslutte pent i stedet for å krasje.»

Du kan anta at les_salg og meny allerede finnes.

📝Oppgave 10
Eksamensnivå, sjanger H…

Utvid loppemarkedsappen.

a) Skriv beste_bod(tabell) som returnerer bodnummeret med høyest omsetning, ved å gjenbruke omsetning_per_bod.

b) Skriv lagre_binaer(tabell, filnavn) som lagrer omsetningsordboka på binærfil.

c) Utvid menyen med to nye valg, 3 for beste bod og 4 for binærlagring, uten å skrive om de gamle grenene.

d) Forklar med én setning hvorfor meny ikke trengte noen endring utover de to nye grenene.

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.