Eksamen

13. mai 2024
4 timer
Lukket digital eksamen (med safe exam browser).
Alle skrevne og trykte hjelpemidler tillatt.
 

Oppgavetekster, løsningsforslag og sensorveiledning finner du på denne siden.

  1. Automatisk rettet
  2. Forklaring
  3. Kodeskriving

Fordi eksamen var en lukket digital eksamen uten tilgang til å kjøre koden eller bruke internett, bes sensor ikke gi poengtrekk for forhold som enkelt ville blitt oppdaget og raskt rettet ved kjøring av koden. Dette inkluderer blant annet:

  • manglende import-setninger,
  • manglende kodeord (som f. eks. manglende def foran funksjonsdefinisjoner),
  • feil navn på funksjoner og metoder i standardbiblioteket, i egen kode eller i eksterne moduler (såfremt det fremgår noenlunde av funksjonsnavnet hva kandidaten egentlig mener),
  • feil navn på variabler (f. eks. kalle den samme variabelen både total og sum i ulike deler av koden),
  • enkle syntaks-feil (f. eks. manglende kolon etter if-setninger),
  • og så videre.

Logiske feil skal det likevel (som hovedregel) bli trukket litt for; selv om man kunne ha oppdaget at noe var feil ved kjøring av koden. Dette inkluderer blant annet:

  • presedensfeil,

  • forveksling av indekser og elementer,

  • off-by-one -feil,

  • feil i algoritmer,

  • og så videre.


1 Automatisk rettet
2 Forklaring
(a)  Alfred's to filer for å lage et plot (14 poeng)

Alfred skal plotte en linje, og har skrevet et program over to Python-filer for å få det til. Når Alfred kjører my_script.py krasjer imidlertid programmet hans og viser en feilmelding i terminalen.

my_lineplotter.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from matplotlib import pyplot as plt

def plot_line(line):
    xs = []
    ys = []
    for point in line:
        x = point[0]
        y = point[1]
        xs.append(x)
        ys.append(y)
    plt.plot(xs, ys)

my_script.py

1
2
3
4
5
6
7
from matplotlib import pyplot as plt
from my_lineplotter import plot_line

# Plot linjen (0, 0) -> (1, 2) -> (2, 1) -> (3, 3)
line = [0, 0, 1, 2, 2, 1, 3, 3]
plot_line(line)
plt.show()

Traceback (most recent call last):
  File "/path/to/my_script.py", line 6, in <module>
    plot_line(line)
  File "/path/to/my_lineplotter.py", line 7, in plot_line
    x = point[0]
        ~~~~~^^^
TypeError: 'int' object is not subscriptable

Illustrasjon av ønsket oppførsel

Forklar:

  • Hva betyr feilmeldingen? Forklar hvilken informasjonen feilmeldingen gir oss.
  • Hvordan kan du rette programmet slik at det viser et plot som vist i illustrasjonen i stedet for å krasje? Forklar nøyaktig hvor og hvordan du planlegger å gjøre rettelsen.

Bruk vanlig språk; du skal ikke skrive store kodesnutter. Samtidig må du være detaljert og presis nok til å overbevise sensor om at du forstår hva du snakker om.

Maks 600 ord.

Feilmeldingen TypeError: 'int' object is not subscriptable betyr at programmet forsøker å indeksere et heltall. Å indeksere er noe man vanligvis gjør med lister, tupler og strenger for å hente ut enkeltverdier på en gitt indeks/posisjon (det er også mulig å indeksere et oppslagsverk, siden en nøkkel i et oppslagsverk er en generalisert form for indeks). En int er derimot bare et enkelt tall, og det gir ikke mening å indeksere et enkelt tall.

Feilmeldingen forteller oss altså at variabelen point er en int når vi forsøker å gjennomføre linje 7 i my_lineplotter.py. Vi ser også av feilmeldingen at dette er en linje i funksjonen plot_line. Feilmeldingen forteller oss dessuten at funksjonskallet til plot_line ble gjort fra linje 6 i hovedprogrammet i my_script.py.

Ved inspeksjon av funksjonen plot_line ser vi at funksjonen forventer at line er en liste av «punkter», hvor hvert punkt er en tuple (evt. liste) av to elementer (x, y). Ved inspeksjon av my_script.py ser vi derimot at argumentet til plot_line er en liste av heltall, og ikke en liste av tupler. Feilen oppstår altså fordi koden i my_lineplotter.py og my_script.py ikke er enig med hverandre om hvilket format line skal ha; my_lineplotter.py forventer en liste av tupler, mens my_script.py gir en liste av heltall hvor annethvert tall er x-koordinat og det neste er y-koordinat.

Feilen kan rettes på én av to måter:

  • Endre line i my_script.py til å være en liste av tupler, slik at argumentet til plot_line får riktig format. Dette kan gjøres ved å endre linje 6 i my_script.py til line = [(0, 0), (1, 2), (2, 1), (3, 3)]. Dette er den enkleste løsningen, og den som er mest i tråd med hvordan funksjonen plot_line er skrevet. Ingen endringer behøves i my_lineplotter.py.

  • Endre plot_line i my_lineplotter.py til å håndtere en liste av heltall hvor annethvert tall skal legges til i xs og i ys. En slik endring gjør at vi må endre en vesentlig del av løkken i plot_line. Det er flere veier til mål; én mulighet er å benytte en løkke som itererer over indekser i stedet for elementer, og henter ut x og y ved å indeksere line med line[i] og line[i+1] i stedet for point[0] og point[1]. En slik løkke må kun gå gjennom partallsindekser, dvs benytte range(0, len(line), 2) som iterabel i for-løkken. Ingen endringer behøves i my_script.py.

Bokstavelig tolkning av feilmeldingen (3 poeng)

  • Krasjen skjer på linje 7 i my_lineplotter.py (1 poeng)
  • Krasjen skjer fordi point er en int (1 poeng)
  • plot_line ble kalt fra linje 6 i my_script.py (1 poeng)

Forståelse av feilen (5 poeng)

  • Forståelse av hva indeksering er (2 poeng)
  • Feilen skyldes at line i my_script.py er en liste av heltall, mens plot_line forventer en liste av «punkter» representert som tupler eller lister (3 poeng)

Forslag til rettelse (3 poeng)

  • Gir en presis forklaring på minst én mulig løsning (3 poeng)

Helhetsvurdering (3 poeng)

  • Meningsfulle forklaringer av sammenhenger.
  • God bruk av faguttrykk.
  • Finner ikke feil som ikke er feil.
  • Kategorien vurderes helhetlig.
(b)  Gi bedre navn til funksjoner og variabler (16 poeng)
def g(b, r):
    s = 0
    for e in b:
        if e == r:
            s += 1
    return s
    
def f(a):
    x = None
    for y in a:
        if x is None:
            x = y
        elif g(a, y) > g(a, x):
            x = y
    return x 

Hva slags funksjoner er dette?

  • Forklar hva funksjonene gjør.
  • Koden over har dårlige variabel- og funksjonsnavn, som gjør den vanskelig å lese og forstå. Forklar rollen til de ulike variablene i koden over, og foreslå nye, selvbeskrivende navn for variabel- og funksjonsnavnene. Dersom du mener det ikke er nødvendig å endre et variabelnavn, forklar hvorfor variabelnavnet er egnet slik det allerede er.

8 poeng: forklar den øverste funksjonen, og gi bedre variabel- og funksjonsnavn til:

  • g
  • b
  • r
  • s
  • e

8 poeng: forklar den nederste funksjonen, og gi bedre variabel- og funksjonsnavn til:

  • f
  • a
  • x
  • y

Maks 600 ord.

Den øverste funksjonen (g)

Denne funksjonen teller antall forekomster av et element r i en liste b (kan også være en annen type samling, f. eks. en streng eller tuple). Variabelen s er en teller som økes med 1 for hver forekomst av r i b.

Forslag til nye navn
gcount_occurrences
bcollection
relement_to_count
scount
eDette variabelnavnet kan være egnet slik det er. Det er en vanlig konvensjon å bruke e som navn på iteranden i en for-løkke over elementer i en generell samling. Gode alternativer kan også være element, current_element, item etc.

Den nederste funksjonen (f)

Denne funksjonen finner det elementet som forekommer flest ganger i en liste a (kan også være en annen type samling, f. eks. en streng eller tuple). Funskjonen går gjennom alle elementene i a, og oppdaterer x til å hele tiden peke på det elementet vi har sett så langt som forekommer flest ganger. Variabelen y er en midlertidig peker til et element i a som sammenlignes med x for å finne ut om y forekommer flere ganger enn x.

Forslag til nye navn
fmost_common_element
aDette variabelnavnet kan være egnet slik det er, da det er en vanlig konvensjon å bruke a som navn på en generell liste av elementer. Gode alternativer kan være collection, elements etc.
xmost_common_so_far
ycurrent_element

Den øverste funksjonen (g)

  • 2 poeng: forklaring/forståelse («teller antall forekomster av r i b»).
  • 3 poeng: godt funksjonsnavn og gode parameternavn
    • Eksempler på gode funksjonsnavn: count_occurrences, count_element, count_element_in_collection, number_of_occurrences, count, og lignende.
    • Eksempler på gode navn for b: collection, elements, items, chars, word, lst, sequence, data, container, objects, entries, values, characters, letters, words, iterable, etc. Aksepterer også a.
    • Eksempler på gode navn for r: element_to_count, element, item, letter, obj, entry, value, etc.
  • 1 poeng: lokale variabelnavn
    • Eksempler på gode navn for s: count, counter, occurrences, occurrence, n, total, tally, cnt, occur, num_occurrences, num_occ, num, n_occurrences, etc. Aksepterer også result, res og lignende.
    • Eksempler på gode navn for e: element, item, entry, value, obj, letter, eller forklaring på at e er fornuftig slik det er.
  • 2 poeng: helhetsvurdring (bruk av faguttrykk, forståelse av sammenhenger etc).

Den nederste funksjonen (f)

  • 2 poeng: forklaring/forståelse («finner elementet som forekommer flest ganger i a»).
  • 2 poeng: godt funksjonsnavn og parameternavn
    • Eksempler på gode funksjonsnavn: most_common_element, find_most_common, get_most_common_item, most_common og lignende.
    • Eksempler på gode navn for a: collection, elements, items, chars, lst, sequence, data, container, objects, entries, values, characters, letters, iterable, etc. Aksepterer også en forklaring på at a er fornuftig slik det er.
  • 2 poeng: lokale variabelnavn
    • Eksempler på gode navn for x: most_common_so_far, most_common, current_most_common. Aksepterer også best, winner, result, res og lignende.
    • Eksempler på gode navn for y: current_element, element, e, candidate, current, item, entry, value, obj, letter, e etc.
  • 2 poeng: helhetsvurdering (bruk av faguttrykk, forståelse av sammenhenger etc).
3 Kodeskriving
(a)  Gjenskap minnets tilstand (5 poeng)
Illustrasjon av variabler og minnets tilstand slik det skal gjenskapes

Skriv en kodesnutt slik at minnets tilstand blir som vist over. Du vil ikke få trekk i poeng dersom du definerer andre variabler i tilegg.

Klikk på «se steg» -knappen for å verifisere at denne koden gir riktig bilde av minnet.

a = [3, 3, 3]
b = [a, 3, 3]
  • 1 poeng hvis a er korrekt (a == [3, 3, 3]).
  • 1 poeng hvis b er korrekt (b == [[3, 3, 3], 3, 3]).
  • 1 poeng hvis a er lik b[0] (a == b[0]).
  • 2 poeng hvis a og b[0] viser til samme objekt (a is b[0]).

Sensor har anledning til å gjøre en skjønnsmessig justering i tilfeller der det er gjort feil disse testene ikke tar høyde for. Poengsummen som beskrevet over kan regnes ut med følgende kode:

points = sum([
    a == [3, 3, 3],
    b == [[3, 3, 3], 3, 3],
    a == b[0],
    (a is b[0]) * 2,
])
# PS: i kontekst av matematisk aritmetikk regnes True som 1 og False som 0.
(b)  Gjenskap minnets tilstand (5 poeng)
Illustrasjon av variabler og minnets tilstand slik det skal gjenskapes

Skriv en kodesnutt slik at minnets tilstand blir som vist over. Du vil ikke få trekk i poeng dersom du definerer andre variabler i tilegg.

Klikk på «se steg» -knappen for å verifisere at denne koden gir riktig bilde av minnet.

a = [[1, 2, 3], [1, 2, 3]]
b = [[1, 2, 3]] * 2

temp1 = [1, 2, 3]
temp2 = [1, 2, 3]
temp3 = [1, 2, 3]

a = [temp1, temp2]
b = [temp3, temp3]
a = [[1, 2, 3], [1, 2, 3]]
b = [[1, 2, 3]]
b.append(b[0])
a = []
a.append([1, 2, 3])
a.append([1, 2, 3])

b = []
b.append([1, 2, 3])
b.append(b[0])

  • 1 poeng hvis a er korrekt (a == [[1, 2, 3], [1, 2, 3]]).
  • 1 poeng hvis b er korrekt (b == [[1, 2, 3], [1, 2, 3]]).
  • 1 poeng hvis b[0] og b[1] viser til samme objekt (b[0] is b[1]).
  • 1 poeng hvis a[0] og a[1] er like, men viser til ulike objekter (a[0] == a[1] and a[0] is not a[1]).
  • 1 poeng hvis ingen av elementene i b viser til et element i a (b[0] is not a[0] and b[0] is not a[1] and b[1] is not a[0] and b[1] is not a[1]), selv om alle elementene i a og b er like.

Sensor har anledning til å gjøre en skjønnsmessig justering i tilfeller der det er gjort feil disse testene ikke tar høyde for. Poengsummen som beskrevet over kan regnes ut med følgende kode:

points = sum([
    a == [[1, 2, 3], [1, 2, 3]],
    b == [[1, 2, 3], [1, 2, 3]],
    b[0] is b[1],
    (a[0] == a[1]) and (a[0] is not a[1]),
    (
          ((b[0] is not a[0]) and (b[0] == a[0])) 
      and ((b[0] is not a[1]) and (b[0] == a[1]))
      and ((b[1] is not a[0]) and (b[1] == a[0]))
      and ((b[1] is not a[1]) and (b[1] == a[1]))
    )
])
# PS: i kontekst av matematisk aritmetikk regnes True som 1 og False som 0.
(c)  Hotellets logg (25 poeng)

Anta at du har en aktivitetslogg for et hotell. Aktivitetsloggen er en semikolon-separert csv-fil der hver rad representerer én hendelse. Kolonnene i csv-filen er

  • tidspunkt (formattert som yyyy-MM-dd HH:mm)
  • hendelsestype (enten «innsjekk» eller «utsjekk»)
  • romnummer
  • navn på gjesten

Eksempel på de første par linjene i filen:

tidspunkt;hendelsestype;romnummer;navn på gjest
2024-01-01 16:34;innsjekk;201;Donald
2024-01-01 17:15;innsjekk;302;Dolly
2024-01-02 09:14;utsjekk;201;Donald
2024-01-02 10:45;innsjekk;201;Magica fra Tryll
2024-01-02 10:59;utsjekk;302;Dolly

Anta at hendelsene allerede ligger i sortert rekkefølge basert på tidspunkt, og at ingen var sjekket inn på hotellet på tidspunktet loggen begynner. Du kan også anta at loggfilen er konsistent (utsjekk har alltid samme navn på personen som forrige gang det var innsjekk på samme romnummer; og det er alltid annenhver innsjekk og utsjekk om vi kun ser på ett rom av gangen).

Det er tre deloppgaver:

  • (5 poeng) Skriv en funksjon load_logfile(path) hvor path er en parameter for filnavnet til loggfilen. Funksjonen skal returnere innholdet i filen på et format egnet for videre prosessering i Python. Du kan velge selv om du vil returnere en 2D-liste eller en liste av oppslagsverk.

  • (10 poeng) Skriv en funksjon current_guest_count(table) hvor table er en liste på det formatet du valgte i forrige deloppgave. Funksjonen skal returnere antallet gjester som er sjekket inn på hotellet når loggfilen tar slutt.

Eksempel: dersom filen kun bestod av linjene i eksempelet over, skal funksjonen returnere 1: fordi det er én gjest (Magica fra Tryll) som er sjekket inn når filen tar slutt.

  • (10 poeng) Skriv en funksjon get_guests(table, time) hvor table er en liste på det formatet du valgte i første deloppgave, og time er en streng som representerer et tidspunkt på samme format som brukes i loggfilen. Funksjonen skal returnere en liste over hvilke gjester som er sjekket inn og hvilke rom de befinner seg på. Mer konkret: funksjonen skal returnere en liste av tupler, der hver tuple består av (navn, romnummer) for en person som er sjekket inn på det gitte tidspunktet.

Eksempel: dersom filen kun bestod av linjene i eksempelet over, og det angitte tidspunktet er '2024-01-02 10:50' skal funksjonen returnere listen [('Magica fra Tryll', '201'), ('Dolly', '302')].

import csv
from pathlib import Path

# Svaret på denne deloppgaven er basert på kursnotater om
# standardbiblioteket, avsnitt om CSV/DictReader
def load_file(path):
    with Path(path).open('rt', encoding='utf-8', newline='') as f:
        reader = csv.DictReader(f, delimiter=';')
        return list(reader)
def current_guest_count(table):
    count = 0
    for event in table:
        if event['hendelsestype'] == 'innsjekk':
            count += 1
        elif event['hendelsestype'] == 'utsjekk':
            count -= 1
    return count
def get_guests(table, time):
    currently_checked_in = {}
    for event in table:
        if event['tidspunkt'] > time:
            break
        elif event['hendelsestype'] == 'innsjekk':
            currently_checked_in[event['romnummer']] = event['navn på gjest']
        elif event['hendelsestype'] == 'utsjekk':
            currently_checked_in[event['romnummer']] = None

    result = []
    for room in currently_checked_in:
        if currently_checked_in[room] is not None:
            result.append((currently_checked_in[room], room))
    return result

def load_file(path):
    with open(path, 'r', encoding='utf-8') as f:
        content = f.read()
    lines = content.splitlines()
    table = []
    for line in lines:
        line = line.strip()
        row = line.split(';')
        table.append(row)
    return table
def current_guest_count(table):
    count = 0
    for event in table:
        if event[1] == 'innsjekk':
            count += 1
        elif event[1] == 'utsjekk':
            count -= 1
    return count
def get_guests(table, time):
    currently_checked_in = {}
    for event in table[1:]:
        if event[0] > time:
            break
        elif event[1] == 'innsjekk':
            currently_checked_in[event[2]] = event[3]
        elif event[1] == 'utsjekk':
            currently_checked_in[event[2]] = None

    result = []
    for room in currently_checked_in:
        if currently_checked_in[room] is not None:
            result.append((currently_checked_in[room], room))
    return result

PS: denne løsningen scorer veldig dårlig på lesbarhet og kodestil, og ville blitt trukket alle poengene under punktet ‘helhetsvurdering’ om den ikke var utstyrt med ekstra forklaringer.

def load_file(path):
    with open(path, 'r', encoding='utf-8') as f:
        return [r.strip().split(';') for r in f][1:]
def current_guest_count(table):
    return sum(2*('i' in e[1])-1 for e in table)
def get_guests(table, time):
    d = {}
    for e in [e for e in table if e[0] <= time]:
        d[e[2]] = e[3] if 'i' in e[1] else None
    return [(n, r) for r, n in d.items() if n is not None]

Merk at sammenligningen av tidspunkt er mulig å gjøre som en streng-sammenligning uten å gå via datetime. Dette er mulig siden tidspunktene er formattert med mest signifikante enheter først, og med fast lengde på alle deler av tidspunktet.

load_file (5 poeng)

I deloppgaven om load_file finnes svaret mer eller mindre direkte i kursnotatene som var vedlagt eksamen. Koden må tilpasses til riktig seperator (semikolon) og variabel for filnavn.

  • 1 poeng: åpner filen gitt av path og leser innholdet
  • 1 poeng: deler opp linjene/radene på en eller annen måte (f. eks. .splitlines(), .split('\n') eller konvertering til liste hvis bruk av csv-modulen for å lese filen, list(reader_object))
  • 1 poeng: tar hensyn til at semikolon skiller kolonner (f. eks. .split(';') på hver linje, eller bruk av navngitte parametre hvis bruk av csv-modulen csv.reader(f, delimiter=';') / csv.DictReader(f, delimiter=';'))
  • 1 poeng: returnerer en fornuftig sammensatt datastruktur
  • 1 poeng: helhetsvurdering

Det trekkes ikke poeng for

  • å ikke angi encoding='utf-8' ved åpning av filen (tekstformat er ikke oppgitt).
  • å ikke angi newline='' ved åpning av filen ved bruk av csv-modulen. Selv om dette egentlig alltid skal gjøres når man benytter csv-modulen, vil det fungere fint uten her.
  • å glemme å importere csv -modulen eller Path-klassen fra pathlib-modulen selv om disse brukes.

current_guest_count (10 poeng)

  • 2 poeng: løkke over radene i tabellen
  • 2 poeng: sjekker om hendelsestype er innsjekk eller utsjekk
  • 2 poeng: book-keeping: en eller annen form for telling av antall gjester
  • 4 poeng: helhetsvurdering

En mulig løsning vil være denne funksjonen:

def current_guest_count(table):
    return len(get_guests(table, '9999-12-31 23:59'))

En slik løsning kan gi full pott, men man må da vurdere get_guests-funksjonen i henhold til sensurkriteriene for denne deloppgaven.

get_guests (10 poeng)

  • 2 poeng: fornuftig betingelse benyttet for å ignorere hendelser som skjer etter det gitte tidspunktet
  • 2 poeng: sammenligner tidspunktet i raden med tidspunktet gitt som parameter på en fornuftig måte
  • 3 poeng: book-keeping: en eller annen form for lagring av gjester som er sjekket inn på det gitte tidspunktet, typisk et oppslagsverk med romnummer som nøkkel og navn på gjest som verdi, samt korrekt oppdatering av dette i løkken.
  • 1 poeng: koden håndterer at samme gjest sjekker inn og ut flere ganger
  • 2 poeng: helhetsvurdering

Helhetsvurdering

Helhetsvurdering er basert på helhetlig korrekthet og forståelse av oppgaven.

  • For besvarelser som i grove trekk er korrekte og får god uttelling på de øvrige kulepunktene, kan man gi litt trekk her for logiske feil (hvis noen) og dersom koden har dårlig struktur og/eller er spesielt vaskelig å lese.
  • For besvarelser som får dårlig uttelling på de øvrige kulepunkt i forhold til hva besvarelsen demonstrerer av forståelse, kan sensor her gi en skjønnsmessig oppjustering. Dette gjelder for eksempel i tilfeller der det er skrevet kommentarer som viser intensjonen med en kodesnutt selv om gjennomføringen ikke var vellykket.

Om bruk av datetime-modulen

  • Det er ikke nødvendig å benytte datetime eller andre kompliserte (hjelpe-)metoder for å sammenligne tidspunktene; det er fullt mulig å sammenligne tidspunktene direkte som strenger, siden de er formattert med mest signifikante enheter først og med fast lengde på alle deler av tidspunktet.
  • Det vil ikke gi noe poengtrekk å bruke datetime-modulen for å sammenligne tidspunktene, selv om det er gjort små feil i konvertering fra streng til datetime.
  • Det vil ikke gi poengtrekk å benytte en egen hjelpefunksjon for å sammenligne to tidspunkt. Små detaljfeil i implementasjonen av en slik hjelpefunksjon vil heller ikke gi poengtrekk.
  • Å gjøre en innviklet/komplisert sammenligning av tidspunkter eller konvertering til datetime direkte i løkken uten å benytte en hjelpefunksjon vil gi trekk i helhetsvurderingen for dårlig struktur/lesbarhet.