7.4 DRILL — Ledd 3: analysér — finn maks/beste, tell og gruppér
Kjernen i programmeringsdelen: gå gjennom tabellen/ordboken og finn beste/vinner, tell forekomster, og gruppér — både med eksplisitt løkke og med snarvei.
- Analysen står i 100 % av settene (10 av 10 sittinger i arkivet 2014–2019) og kommer nesten alltid som to–tre deloppgaver à 5–10 %. Til sammen er den mer verdt enn de fire andre leddene lagt sammen.
- Sjangeren heter G — analyse, altså «gå gjennom dataene og finn det oppgaven spør om».
- Tre former dekker nesten alt: finn den beste raden, tell forekomster per nøkkel, og gruppér radene i en ordbok av lister. Rangering og sortering er den fjerde, og den har sitt eget kapittel i kap. 7.5.
- I dette faget er snarveier tillatt og brukt i fasitene: max, min, sum, sorted, .count. Boka viser den eksplisitte løkka først, så snarveien — og sidestiller dem. Begge gir full pott.
Prioritet: må sitte — det høyeste av bokas tre nivåer.
Kapitlet tar ~75 minutter og er delt i seks løkker med tidsanslag. Naturlig pausepunkt etter løkke 3.
Analysen bygger på tre grep du allerede har. Her er de, ferdig oppfrisket.
1. Gå gjennom radene i en tabell (kap. 3.2) — og hopp over overskriftsraden:
tabell = [['navn', 'pris'], ['girservice', 690], ['dekkskift', 250]]
for rad in tabell[1:]:
print(rad[0], rad[1])Utskrift:
girservice 690
dekkskift 2502. Kolonnen funnet ved navn (kap. 3.2):
tabell = [['mekaniker', 'type', 'minutter', 'pris']]
print(tabell[0].index('pris'))Utskrift:
33. Telleren med get-default (kap. 3.5):
d = {}
for navn in ['Rune', 'Selma', 'Rune']:
d[navn] = d.get(navn, 0) + 1
print(d)Utskrift:
{'Rune': 2, 'Selma': 1}I tillegg forutsetter kapitlet kap. 7.2 og kap. 7.3, som leverte den ferdige tabellen analysen jobber på.
Løkke 1 — finn den beste raden (~14 min)
«Hvem er raskest?», «hvilket produkt er dyrest?», «hvilket parti leder?» — alle tre er den samme løkka, og den er verdt å kunne skrive uten å tenke.
Fem linjer, og de er de samme uansett domene:
beste = None
beste_verdi = None
for rad in tabell[1:]:
v = rad[k]
if beste_verdi is None or v > beste_verdi:
beste = rad
beste_verdi = v
return besteBlokken over er mønsteret; den ferdige funksjonen står i eksempel 1.
Fire ting å legge merke til:
1. To variabler, ikke én: raden du skal returnere, og verdien du sammenligner med. Uten den andre må du hente beste[k] i hver runde.
2. None som startverdi på begge. Da vinner den første raden alltid, uansett hvilke tall dataene inneholder.
3. beste_verdi is None or … — venstre side sjekkes først, så v > None blir aldri forsøkt. Python stopper å regne på et or-uttrykk så snart en side er sann.
4. tabell[1:] når tabellen har overskriftsrad. Glemmer du det, sammenligner du et tall med teksten 'pris'.
Skal du finne den minste, snur du > til <. Ingenting annet endres.
Testen som lar den første raden alltid vinne, uten at du må behandle den som et spesialtilfelle.
Alternativene har hver sin svakhet:
- beste_verdi = 0 — ingen rad vinner hvis alle verdiene er negative, og funksjonen returnerer None uten å si fra.
- beste_verdi = tabell[1][k] — krasjer med IndexError på en tabell som bare har overskriftsraden.
- beste_verdi = -1000000 — virker som regel, men er en gjetning om dataene, og den kan slå feil.
None virker i alle tre tilfellene og koster ingenting. Bruk is None og ikke == None — begge fungerer, men is er formen Python-miljøet bruker, og den er raskere å lese.
Les oppgaveteksten to ganger her. «Returnerer den dyreste raden» og «returnerer navnet på den dyreste» er to forskjellige bestillinger, og forskjellen er ett tegn i koden:
- return beste gir hele raden: ['Selma', 'ramme', 120, 1450]
- return beste[0] gir navnet: 'Selma'
Sier oppgaven ingenting, er hele raden det tryggeste — den som kaller, kan alltid plukke ut feltet selv, men kan ikke få tilbake det som er kastet.
Skal du gi både raden og verdien, returnerer du to verdier: return beste, beste_verdi, og den som kaller pakker ut med rad, verdi = finn_beste(tabell).
oppdrag.txt har overskriftsraden mekaniker;type;minutter;pris. Skriv dyreste_oppdrag(tabell) som returnerer hele raden for det dyreste oppdraget. Kolonnen skal finnes ved navn.»Slik ser fila ut:
Innholdet i oppdrag.txt:
mekaniker;type;minutter;pris
Rune;girservice;45;690
Selma;dekkskift;20;250
Rune;bremsejustering;30;390
Selma;girservice;50;690
Tuva;dekkskift;25;250
Selma;ramme;120;1450Innlesingen er den fra kap. 7.2, og analysen tar tabellen som parameter:
def dyreste_oppdrag(tabell):
k = tabell[0].index('pris')
beste = None
beste_verdi = None
for rad in tabell[1:]:
if beste_verdi is None or rad[k] > beste_verdi:
beste = rad
beste_verdi = rad[k]
return beste
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
print(dyreste_oppdrag(tabell))Utskrift:
['Selma', 'ramme', 120, 1450]Slik ville sensor sett på det. Deloppgaven er typisk 10 %, og fem ting gir uttelling hver for seg:
| Grep | Hvorfor det teller |
|---|---|
tabell[0].index('pris') | kolonnen ved navn — fullscore-kravet fra kap. 7.6 |
tabell[1:] | overskriftsraden holdt utenfor sammenligningen |
beste_verdi is None | virker også på negative verdier og på tom tabell |
beste = rad og beste_verdi = rad[k] | begge oppdateres i samme if |
return beste | funksjonen returnerer, den skriver ikke ut |
Flere korrekte løsninger:
max(tabell[1:], key=lambda rad: rad[k]) gir det samme på én linje, og den kommer i løkke 2. Begge gir full pott.Kantfeilen: to oppdrag til 1450 kroner ville gitt den første av dem, fordi
> er streng. Vil du ha den siste, bruker du >=. Ingen av delene er galt — men skriv én setning om valget hvis oppgaven ikke sier noe.(Innstegsoppgave, sjanger B — kodesporing, altså «hva skriver dette programmet ut?».) Hva skriver programmet ut?
tall = [-8, -3, -19]
beste = 0
for t in tall:
if t > beste:
beste = t
print(beste)korteste_oppdrag(tabell) som returnerer navnet på mekanikeren som har utført oppdraget med færrest minutter. Både mekanikerkolonnen og minuttkolonnen skal finnes ved navn.»Tabellen har overskriftsrad.
Løkke 2 — snarveien (~12 min)
I dette faget er de innebygde funksjonene ikke juks. De offisielle løsningsforslagene bruker dem fritt, og en løsning med max gir nøyaktig samme uttelling som en med løkke.
Poenget med å lære begge er praktisk: løkka er lettere å skrive riktig for hånd når du er sliten, og snarveien er lettere å skrive riktig når du er trygg.
Gir det største og minste elementet i lista. På ei liste av tall er det rett fram.
priser = [690, 250, 390, 1450]
print(max(priser), min(priser))Utskrift:
1450 250På en 2D-liste sammenligner de radene som lister: første element først, deretter det andre, og så videre. max(tabell) gir altså raden med det alfabetisk siste navnet, ikke den dyreste. Det er nesten aldri det du vil ha — og derfor finnes key.
Begge krasjer med ValueError på ei tom liste.
max(liste, key=f) sammenligner ikke elementene selv, men det f gir for hvert element. Elementet som vinner, returneres helt — bare rangeringen bestemmes av nøkkelen.tabell = [['Rune', 45, 690], ['Selma', 20, 250], ['Selma', 120, 1450]]
print(max(tabell, key=lambda rad: rad[2]))Utskrift:
['Selma', 120, 1450]lambda rad: rad[2] er en navnløs funksjon: «gitt en rad, gi meg element nummer 2». Du kan skrive det samme med def og sende funksjonsnavnet inn — begge er riktige, lambdaen er bare kortere.
Merk at kolonnenummeret må være et tall når lambdaen skrives, så finn det med .index(...) først og bruk variabelen.
sum legger sammen alle tallene i lista, len teller elementene. Sammen gir de gjennomsnittet.priser = [690, 250, 390, 1450]
print(sum(priser))
print(sum(priser) / len(priser))Utskrift:
2780
695.0sum virker bare på tall — er kolonnen fortsatt tekst, får du en TypeError. Det er feilkode #1, glemt konvertering, som viser seg her i stedet for i innlesingen.
For å bruke dem på en tabellkolonne må du trekke kolonnen ut først: [rad[k] for rad in tabell[1:]].
Løs tre spørsmål om oppdrag.txt både med eksplisitt løkke og med snarvei: dyreste oppdrag, samlet omsetning og gjennomsnittlig varighet.
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
k = tabell[0].index('pris')
print(max(tabell[1:], key=lambda rad: rad[k]))Utskrift:
['Selma', 'ramme', 120, 1450]Samlet omsetning — begge former:
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
k = tabell[0].index('pris')
total = 0
for rad in tabell[1:]:
total = total + rad[k]
print(total)
print(sum([rad[k] for rad in tabell[1:]]))Utskrift:
3720
3720Gjennomsnittlig varighet — begge former:
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
k = tabell[0].index('minutter')
total = 0
antall = 0
for rad in tabell[1:]:
total = total + rad[k]
antall = antall + 1
print(total / antall)
minutter = [rad[k] for rad in tabell[1:]]
print(sum(minutter) / len(minutter))Utskrift:
48.333333333333336
48.333333333333336Slik ville sensor sett på det: de tre parene er sidestilt. Begge gir full pott, og det står uttrykkelig i de offisielle løsningsforslagene at flere korrekte løsninger godtas.
Det praktiske rådet er likevel dette: skriv snarveien når du er trygg på den, og løkka når du er i tvil. En lambda med en indeksfeil i er vanskelig å finne på papir; en løkke på fire linjer kan du spore med fingeren.
Kantfeilen som gjelder begge: en tom tabell gir ValueError fra max og ZeroDivisionError fra snittet. Løkkevarianten gir None og en divisjon på null — altså samme problem, bare med et annet navn. Tomtilfellet må håndteres uansett hvilken form du velger, og det gjøres i løkke 6.
Bruk tabellen fra oppdrag.txt.
a) Skriv én linje som gir raden for det korteste oppdraget, med min og en key.
b) Skriv én linje som gir den samlede tiden i minutter.
c) Forklar med én setning hvorfor min(tabell[1:]) uten key gir feil svar.
Hva skriver programmet ut? Skriv begge linjene.
rader = [['Nordre', 4], ['Sentrum', 11], ['Ostre', 2]]
print(max(rader))
print(max(rader, key=lambda rad: rad[1]))Løkke 3 — tell og summér per nøkkel (~14 min)
«Hvor mange oppdrag har hver mekaniker?» og «hvor mye har hver mekaniker omsatt for?» er det samme mønsteret med ett tegn til forskjell.
— naturlig pausepunkt etter denne løkka —
Fire linjer, og forskjellen mellom å telle og å summere er hva du legger til:
d = {}
for rad in tabell[1:]:
d[rad[k]] = d.get(rad[k], 0) + 1
return dBlokken over er telleren. Bytt + 1 med + rad[j], og du har summeringen i stedet.
Hvorfor d.get(rad[k], 0) og ikke d[rad[k]]: oppslag på en nøkkel som ikke finnes gir KeyError. get gir 0 i stedet, og det er nøyaktig riktig startverdi for både en teller og en sum.
Merk at get ikke setter nøkkelen inn i ordboka — det gjør tilordningen på venstre side. Det er derfor mønsteret må skrives som en tilordning, ikke som et uttrykk.
Bygger {nøkkel: antall} i én gjennomgang av dataene.
d = {}
for navn in ['Rune', 'Selma', 'Rune', 'Selma', 'Tuva', 'Selma']:
d[navn] = d.get(navn, 0) + 1
print(d)Utskrift:
{'Rune': 2, 'Selma': 3, 'Tuva': 1}Legg merke til rekkefølgen i utskriften: nøklene kommer i den rekkefølgen de ble satt inn første gang. Skal svaret være sortert, bruker du sorted(d) når du går gjennom den.
Feilen å unngå er d[navn] += 1 uten get: den krasjer med KeyError første gang navnet dukker opp, fordi det ikke finnes noen gammel verdi å legge 1 til.
Samme mønster, men verdien fra raden legges til i stedet for 1. Bygger {nøkkel: sum}.
rader = [['Rune', 690], ['Selma', 250], ['Rune', 390]]
d = {}
for rad in rader:
d[rad[0]] = d.get(rad[0], 0) + rad[1]
print(d)Utskrift:
{'Rune': 1080, 'Selma': 250}Test på oppgaveteksten: «hvor mange» peker på telleren, «hvor mye til sammen» peker på summeringen. Det er verdt å lese setningen to ganger — de to gir helt forskjellige tall, og begge ser riktige ut.
Skal du ha snittet per nøkkel, trenger du begge ordbøkene: summen delt på antallet.
Skal bare noen av radene telles, står testen inne i løkka, før tellingen:
for rad in tabell[1:]:
if rad[k_type] == 'girservice':
d[rad[k_navn]] = d.get(rad[k_navn], 0) + 1Blokken over er et utdrag fra analysefunksjonen.
Formuleringen i oppgaveteksten er som regel «… som har utført minst ett girservice-oppdrag» eller «… bare oppdrag som tok over en time».
Vær nøye med hva som skal med i ordboka når betingelsen aldri slår til: med denne formen kommer mekanikeren rett og slett ikke med i det hele tatt. Vil du ha alle med, også med 0, må du legge dem inn først med en egen løkke.
To snarveier når du bare skal telle én bestemt verdi, ikke bygge en hel ordbok.
typer = ['girservice', 'dekkskift', 'girservice', 'ramme']
print(typer.count('girservice'))
print('ramme' in typer)
print(typer.count('bremser'))Utskrift:
2
True
0.count(v) gir 0 for noe som ikke finnes — den krasjer aldri. in gir True eller False.
Begge virker på ei flat liste, ikke på en 2D-tabell. Skal du telle i en kolonne, trekker du kolonnen ut først: [rad[k] for rad in tabell[1:]].count('girservice').
oppdrag_per_mekaniker(tabell) som returnerer {mekaniker: antall}, og omsetning_per_mekaniker(tabell) som returnerer {mekaniker: sum pris}. Skriv i tillegg antall_av_type(tabell, type) som teller hvor mange oppdrag som er av en bestemt type.»def oppdrag_per_mekaniker(tabell):
k = tabell[0].index('mekaniker')
d = {}
for rad in tabell[1:]:
d[rad[k]] = d.get(rad[k], 0) + 1
return d
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
print(oppdrag_per_mekaniker(tabell))Utskrift:
{'Rune': 2, 'Selma': 3, 'Tuva': 1}Summeringen — ett tegn til forskjell:
def omsetning_per_mekaniker(tabell):
k_navn = tabell[0].index('mekaniker')
k_pris = tabell[0].index('pris')
d = {}
for rad in tabell[1:]:
d[rad[k_navn]] = d.get(rad[k_navn], 0) + rad[k_pris]
return d
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
print(omsetning_per_mekaniker(tabell))Utskrift:
{'Rune': 1080, 'Selma': 2390, 'Tuva': 250}Selma har tre oppdrag og 2390 kroner; Rune har to oppdrag og 1080.
Tellingen av én type, begge former:
def antall_av_type(tabell, type_navn):
k = tabell[0].index('type')
antall = 0
for rad in tabell[1:]:
if rad[k] == type_navn:
antall = antall + 1
return antall
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
print(antall_av_type(tabell, 'girservice'))
k = tabell[0].index('type')
print([rad[k] for rad in tabell[1:]].count('girservice'))Utskrift:
2
2Slik ville sensor sett på det: at antall_av_type tar typen som parameter er verdt poeng i seg selv — da kan den kalles for alle typene i en senere deloppgave, i stedet for at du skriver én funksjon per type.
De to formene er sidestilt. Begge gir full pott.
Kantfeilen: antall_av_type(tabell, 'Girservice') med stor forbokstav gir 0. Skal søket være ufølsomt for store og små bokstaver, sammenligner du rad[k].lower() == type_navn.lower(). Sier ikke oppgaven noe, skriv én setning om at du antar eksakt treff.
Fem feller, og de fire første er stumme:
- Startverdi 0 i «finn beste». Ingen rad vinner når alle verdiene er negative. Bruk None.
- #4 — hardkodet kolonneindeks. rad[3] svarer feil hvis kolonnene kommer i en annen rekkefølge. Bruk tabell[0].index(navn).
- Glemt tabell[1:]. Overskriftsraden blir med i analysen, og 'pris' sammenlignes med et tall — som gir TypeError hvis du er heldig, og et feil antall hvis du bare teller.
- max uten key på en 2D-liste. Radene sammenlignes som lister, og den alfabetisk siste vinner.
- d[k] += 1 uten get. Denne krasjer med KeyError første gang nøkkelen dukker opp, og er derfor den snilleste av de fem.
avviksmeldinger.txt har overskriftsraden bydel;type;dager. Skriv meldinger_per_bydel(tabell) som returnerer {bydel: antall}, og dager_per_bydel(tabell) som returnerer {bydel: sum dager}.»Begge kolonnene skal finnes ved navn.
lange_oppdrag_per_mekaniker(tabell, grense) som returnerer {mekaniker: antall}, men teller bare oppdrag som tok flere minutter enn grense.»Test med grensen 30.
Løkke 4 — gruppér (~14 min)
Den tredje analyseformen: i stedet for ett tall per nøkkel skal du ta vare på alle radene som hører til nøkkelen. Da er verdien i ordboka ei liste.
Fem linjer, og den tredje er den som glipper oftest:
d = {}
for rad in tabell[1:]:
if rad[k] not in d:
d[rad[k]] = []
d[rad[k]].append(rad)
return dBlokken over er mønsteret; den ferdige funksjonen står i eksempel 4.
Den indre lista må finnes før du legger noe i den. d[rad[k]].append(...) slår først opp d[rad[k]], og et oppslag på en ukjent nøkkel gir KeyError. Testen if … not in d er derfor ikke pynt, den er nødvendig.
Hva du legger inn, styres av oppgaveteksten: hele raden (rad), ett felt (rad[j]), eller en kombinasjon. «Alle rettene som bruker ingrediensen» betyr rettens navn, ikke hele oppskriften.
Gjør de to første linjene i grupperingen på én: slår opp nøkkelen, og setter inn standardverdien først hvis den mangler. Returnerer verdien i begge tilfeller.
d = {}
d.setdefault('Rune', []).append('girservice')
d.setdefault('Rune', []).append('bremser')
d.setdefault('Selma', []).append('dekkskift')
print(d)Utskrift:
{'Rune': ['girservice', 'bremser'], 'Selma': ['dekkskift']}Formen er sidestilt med den eksplisitte if … not in d-varianten, og begge gir full pott.
Merk forskjellen fra get: setdefault setter nøkkelen inn i ordboka, get gjør det ikke. Det er nettopp derfor setdefault virker for gruppering og get for telling.
Tre varianter, og oppgaveteksten avgjør hvilken:
- d[nøkkel].append(rad) — hele raden. Nyttig når en senere deloppgave skal regne på gruppen.
- d[nøkkel].append(rad[j]) — ett felt. «Alle typene hver mekaniker har jobbet med.»
- d[nøkkel].append(rad[j]) med duplikatsjekk — når oppgaven sier «uten gjentakelser».
Den siste er et eksplisitt fullscore-krav i flere sett, og den drilles i kap. 7.6. Formen er if rad[j] not in d[nøkkel]: før append.
Les returtypen i oppgaveteksten nøye: {mekaniker: ['girservice', 'ramme']} og {mekaniker: [['Selma', 'ramme', 120, 1450]]} ser like ut i beskrivelsen, men er helt forskjellige strukturer.
typer_per_mekaniker(tabell) som returnerer {mekaniker: [type, type, …]} med alle oppdragstypene mekanikeren har utført. Skriv deretter rader_per_type(tabell) som grupperer hele radene på oppdragstype.»def typer_per_mekaniker(tabell):
k_navn = tabell[0].index('mekaniker')
k_type = tabell[0].index('type')
d = {}
for rad in tabell[1:]:
if rad[k_navn] not in d:
d[rad[k_navn]] = []
d[rad[k_navn]].append(rad[k_type])
return d
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
print(typer_per_mekaniker(tabell))Utskrift:
{'Rune': ['girservice', 'bremsejustering'], 'Selma': ['dekkskift', 'girservice', 'ramme'], 'Tuva': ['dekkskift']}Gruppering av hele rader, med setdefault:
def rader_per_type(tabell):
k = tabell[0].index('type')
d = {}
for rad in tabell[1:]:
d.setdefault(rad[k], []).append(rad)
return d
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
d = rader_per_type(tabell)
print(d['girservice'])
print(len(d['dekkskift']))Utskrift:
[['Rune', 'girservice', 45, 690], ['Selma', 'girservice', 50, 690]]
2Hvorfor gruppering er verdt å kunne: når radene først er samlet per nøkkel, er de neste spørsmålene ett kall hver.
grupper = {'girservice': [['Rune', 'girservice', 45, 690], ['Selma', 'girservice', 50, 690]],
'dekkskift': [['Selma', 'dekkskift', 20, 250], ['Tuva', 'dekkskift', 25, 250]]}
for type_navn in sorted(grupper):
minutter = [rad[2] for rad in grupper[type_navn]]
print(type_navn.ljust(14), len(minutter), sum(minutter) / len(minutter))Utskrift:
dekkskift 2 22.5
girservice 2 47.5Slik ville sensor sett på det: den ene linja if rad[k] not in d: d[rad[k]] = [] er verdt poengene sine alene, fordi koden krasjer uten den. Sensor ser etter at du vet at den indre lista må opprettes.
setdefault gir samme uttelling. Velg den du husker riktig — den eksplisitte formen er lettere å feilsøke, den korte er raskere å skrive.
Kantfeilen: d[rad[k]] = rad i stedet for .append(rad) gir ingen feilmelding, men bare den siste raden per nøkkel. Det er den samme fellen som i kap. 7.2 — en ordbok har én verdi per nøkkel.
avviksmeldinger.txt. Skriv bydeler_per_type(tabell) som returnerer {type: [bydel, bydel, …]} — hvilke bydeler som har meldt inn hver type avvik.»snitt_per_type(tabell) som returnerer {type: gjennomsnittlig varighet} for oppdragene på verkstedet. Bruk rader_per_type fra eksempel 4 i stedet for å gå gjennom tabellen på nytt.»Rund av til én desimal i utskriften, ikke i ordboka.
Løkke 5 — analysér ordboka (~12 min)
Deloppgavene kommer ofte parvis: først «bygg ordboka», så «finn den beste nøkkelen i den». Den andre er «finn beste» én gang til, bare over nøkler i stedet for rader.
Samme mønster som «finn beste rad», men løkka går over nøklene og verdien hentes med oppslag:
d = {'Rune': 1080, 'Selma': 2390, 'Tuva': 250}
beste = None
beste_verdi = None
for navn in d:
if beste_verdi is None or d[navn] > beste_verdi:
beste = navn
beste_verdi = d[navn]
print(beste, beste_verdi)Utskrift:
Selma 2390Snarveien er max(d, key=d.get): gå gjennom nøklene, og bruk d.get som nøkkelfunksjon. Den gir nøkkelen, ikke verdien — vil du ha begge, henter du d[beste] etterpå.
for navn in d: går over nøklene, ikke verdiene. for verdi in d.values(): går over verdiene, men da mister du hvilken nøkkel de hørte til.
Nøklene i en ordbok kommer i den rekkefølgen de ble satt inn. Det er forutsigbart, men sjelden det du vil vise en bruker.
d = {'Selma': 3, 'Rune': 2, 'Tuva': 1}
print(list(d))
print(sorted(d))Utskrift:
['Selma', 'Rune', 'Tuva']
['Rune', 'Selma', 'Tuva']sorted(d) gir nøklene som ei sortert liste, uten å endre ordboka. Bruk den i alle utskriftsløkker — da blir svaret ditt det samme hver gang, og det er lettere for sensor å lese.
Skal du sortere på verdien i stedet, er formen sorted(d, key=d.get), og den kommer i kap. 7.5.
omsetning_per_mekaniker fra eksempel 3. Skriv beste_mekaniker(tabell) som returnerer navnet på den med høyest omsetning, og skriv ut hele oversikten sortert alfabetisk.»def omsetning_per_mekaniker(tabell):
k_navn = tabell[0].index('mekaniker')
k_pris = tabell[0].index('pris')
d = {}
for rad in tabell[1:]:
d[rad[k_navn]] = d.get(rad[k_navn], 0) + rad[k_pris]
return d
def beste_mekaniker(tabell):
d = omsetning_per_mekaniker(tabell)
beste = None
beste_verdi = None
for navn in d:
if beste_verdi is None or d[navn] > beste_verdi:
beste = navn
beste_verdi = d[navn]
return beste
tabell = [['mekaniker', 'type', 'minutter', 'pris'],
['Rune', 'girservice', 45, 690],
['Selma', 'dekkskift', 20, 250],
['Rune', 'bremsejustering', 30, 390],
['Selma', 'girservice', 50, 690],
['Tuva', 'dekkskift', 25, 250],
['Selma', 'ramme', 120, 1450]]
print(beste_mekaniker(tabell))Utskrift:
SelmaSnarveien for det samme:
d = {'Rune': 1080, 'Selma': 2390, 'Tuva': 250}
print(max(d, key=d.get))Utskrift:
SelmaOversikten, sortert:
d = {'Rune': 1080, 'Selma': 2390, 'Tuva': 250}
for navn in sorted(d):
print(navn.ljust(8) + str(d[navn]).rjust(6))Utskrift:
Rune 1080
Selma 2390
Tuva 250Slik ville sensor sett på det: beste_mekaniker som kaller omsetning_per_mekaniker er nøyaktig den kjeden deloppgavene er bygget for. En besvarelse som går gjennom tabellen på nytt inne i beste_mekaniker gir riktig svar, men taper gjenbrukspoengene — og den er lengre å skrive.
Antakelsen å skrive ned: ved lik omsetning returnerer denne løsningen den som ble sett først i ordboka. Én setning om det, så er valget dokumentert.
Kantfeilen: en tom ordbok gir None fra løkkevarianten og ValueError fra max. Løkka er altså litt snillere her — men begge trenger et bevisst valg, og det er tema i neste løkke.
{bydel: sum dager} fra oppgave 5. Skriv tregeste_bydel(ordbok) som returnerer navnet på bydelen med flest dager til sammen, og total_dager(ordbok) som returnerer summen over alle bydelene.»Begge funksjonene skal ta ordboka som parameter, ikke tabellen.
snitt_per_mekaniker(tabell) som returnerer {mekaniker: gjennomsnittspris}. Bygg den av to ordbøker: én med antall oppdrag og én med samlet omsetning, begge gjenbrukt fra eksempel 3.»Løs den uten å gå gjennom tabellen mer enn de to gangene de to hjelpefunksjonene gjør det.
Løkke 6 — analysen som tåler en tom tabell (~9 min)
Den siste biten før robusthetskapitlet: hva skal en analysefunksjon svare når det ikke finnes noe å analysere?
Fire spørsmål, ett svar hver, og alle fire skal besvares i koden eller i én setning:
| Situasjon | Rimelig returverdi |
|---|---|
| tom tabell i «finn beste» | None |
| tom tabell i en teller | {} — den tomme ordboka, som kommer av seg selv |
| «ikke funnet» i et søk | None eller -1, avhengig av om du returnerer en verdi eller en posisjon |
| tom liste i et gjennomsnitt | None — en divisjon på null krasjer |
Mønsteret med
None som startverdi gir de to første gratis: løkka kjører aldri, og funksjonen returnerer None og {} helt av seg selv. Det er en av grunnene til at mønsteret er skrevet slik.Snarveiene gjør det ikke:
max([]) og sum([]) / len([]) krasjer begge. Bruker du dem, må tomtesten skrives eksplisitt.Test dyreste_oppdrag fra eksempel 1 på tre tabeller: en vanlig, en med bare overskriftsrad, og en der alle prisene er negative (en tabell over rabatter).
def dyreste_oppdrag(tabell):
k = tabell[0].index('pris')
beste = None
beste_verdi = None
for rad in tabell[1:]:
if beste_verdi is None or rad[k] > beste_verdi:
beste = rad
beste_verdi = rad[k]
return beste
print(dyreste_oppdrag([['type', 'pris'], ['ramme', 1450], ['dekk', 250]]))Utskrift:
['ramme', 1450]Bare overskriftsrad — løkka kjører null ganger, og None faller ut av seg selv:
def dyreste_oppdrag(tabell):
k = tabell[0].index('pris')
beste = None
beste_verdi = None
for rad in tabell[1:]:
if beste_verdi is None or rad[k] > beste_verdi:
beste = rad
beste_verdi = rad[k]
return beste
print(dyreste_oppdrag([['type', 'pris']]))Utskrift:
NoneBare negative verdier — her ville beste_verdi = 0 gitt None, mens None gir riktig svar:
def dyreste_oppdrag(tabell):
k = tabell[0].index('pris')
beste = None
beste_verdi = None
for rad in tabell[1:]:
if beste_verdi is None or rad[k] > beste_verdi:
beste = rad
beste_verdi = rad[k]
return beste
print(dyreste_oppdrag([['type', 'pris'], ['rabatt A', -120], ['rabatt B', -45]]))Utskrift:
['rabatt B', -45]Snarveien på det tomme tilfellet:
tabell = [['type', 'pris']]
if len(tabell) <= 1:
print(None)
else:
print(max(tabell[1:], key=lambda rad: rad[1]))Utskrift:
NoneUten testen ville max krasjet med ValueError: max() arg is an empty sequence.
Slik ville sensor sett på det: at løkkevarianten håndterer to av de tre tilfellene uten en eneste ekstra linje, er nettopp grunnen til at mønsteret er verdt å lære slik det står. Sensor ser etter None som startverdi og is None i testen — det er to konkrete tegn på at kandidaten kan kantetilfellene.
snitt_pris(tabell) som returnerer gjennomsnittsprisen for oppdragene. Funksjonen skal returnere None hvis tabellen ikke har noen datarader, og prisen skal finnes ved navn.»Skriv i tillegg én setning om hvorfor None er et bedre svar enn 0 her.
Bruk tabellen fra avviksmeldinger.txt med kolonnene bydel, type og dager.
a) Skriv verste_type(tabell) som returnerer den avvikstypen som har flest meldinger. Bruk telleren og «finn beste nøkkel».
b) Skriv raskest_behandlet(tabell) som returnerer hele raden for meldingen med færrest dager.
c) Skriv typer_per_bydel(tabell) som returnerer {bydel: [type, …]} uten duplikater.
Alle tre skal finne kolonnene ved navn og tåle en tabell uten datarader.
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.