Kapittel 9 Sammensatte datatyper
Så langt i boken har du ofte tydd til lister for å jobbe med informasjon som er relatert. Selv om lister er utmerket for mange oppgaver, er de bare en av flere innebygde datatyper i Python som kan jobbe med flere biter med informasjon. I dette kapittelet skal du lære flere andre datatyper som kalles for sammensatte datatyper73. Dette er datatyper som kan inneholde flere verdier. Kunsten å velge riktig datatype til riktig jobb er sentral for å skrive effektive og leselige programmer. I prosjektet i seksjon 9.8 skal vi se hvordan de nye datatypene kan være nyttige for å jobbe med sensordata for en temperatursensor.
9.1 Introduksjon - Hvordan knytte sammen informasjon?
For en toåring med hammer, så ser alt ut som en spiker.
Betydningen av dette kjente ordtaket er at dersom du bare har et verktøy i verktøykassen din, så vil du prøve å bruke dette på alt. Dette fungerer jo rimelig greit helt til du prøver å slå inn en skrue med en hammer. Så langt er du litt som toåringen når det kommer til sammensatte datatyper. Din hammer er lister, som du alltid vil bruke for å strukturere flere biter med informasjon. Ofte vil dette gå bra, men ikke alltid. La oss ta for oss en situasjon der det ikke går så bra å bruke lister.
Sett at du jobber som studentassistent på et universitet eller høyskole. Du blir satt til å skrive en epost til alle studentene i kurset der du minner hver enkelt student på studentnummeret sitt. Du har både studentnummeret og navnet på hver av studentene. Før du setter i gang med å finne en ekstern pakke i Python for å sende epost så vil du først av alt løse det enklere problemet: Du ønsker å skrive ut beskjeden til terminalen som studentene skal få. Hadde du vært studentassistent i verdens minste fag med bare en student så kunne du jo skrevet dette:
studentnummer = '101749'
studentnavn = 'Eirik Berge'
print(f'Hei {studentnavn}! Du har {studentnummer} som studentnummer.')Dette gikk bra! Men hva hvis det er flere studenter i kurset? Som en flittig toåring med en hammer tyr du til lister og prøver deg på denne koden:
studentnummer = ['101749', '193743', '930273']
studentnavn = [
'Martin Ødegaard',
'Ole Gunnar Solskjær',
'Caroline Graham Hansen'
]
for nummer in studentnummer:
indeks_student = studentnummer.index(nummer)
navn = studentnavn[indeks_student]
print(f'Hei {navn}! Du har {nummer} som studentnummer.')Det er litt irriterende at vi måtte bruke listemetoden .index() for noe så enkelt. Akkurat dette problemet kan du minke med å bruke funksjonen enumerate() som du lærte om i Oppgave 5 i seksjon 5.8.
Et større problem er at vi er helt avhengige av at ikke rekkefølgen i verken studentnummer eller studentnavn endrer seg. Hvis du legger til et nytt navn i studentnavn men glemmer å legge til studentnummeret i studentnummer vil alt bli forskjøvet feil! Vi må altså nå passe på at listene ikke kommer i utakt med hverandre.
Vent nå litt…vet vi egentlig at studentnummeret '101749' passer til navnet 'Martin Ødegaard', studentnummeret '193743' passer til navnet 'Ole Gunnar Solskjær', og studentnummeret '930273' passer til 'Caroline Graham Hansen'? Var det slik jeg skrev det inn i går kveld? Nå er det på tide å tørke kaldsvetten og prøve å huske hvordan du lagde listene studentnummer og studentnavn.
Så mye usikkerheter bare fordi vi valgte feil datatype. I dette tilfellet er nemlig ikke lister et godt valg. Lister er utmerket for data som har en naturlig rekkefølge. Lister er derimot ikke gode til å pare opp to biter med informasjon. For dette er en ordbok74 utmerket. Med en ordbok kunne vi skrevet koden slik:
studenter = {
'101749': 'Martin Ødegaard',
'193743': 'Ole Gunnar Solskjær',
'930273': 'Caroline Graham Hansen'
}
for nummer, navn in studenter.items():
print(f'Hei {navn}! Du har {nummer} som studentnummer.')Her er studenter en ordbok. Du har ikke lært om ordbøker ennå, men det skal du i seksjon 9.4. Selv om du ikke har jobbet med ordbøker så kan du prøve å lese koden over. Ordboken studenter beskriver en sammenheng mellom nummeret '101749' og navnet 'Martin Ødegaard', mellom nummeret '193743' og navnet 'Ole Gunnar Solskjær', og mellom nummeret '930273' og navnet 'Caroline Graham Hansen'. Du får dermed enklere kode når du skal skrive ut beskjeden. I tillegg slipper du å tenke på problemene med to lister i utakt siden vi eksplisitt har beskrevet hvordan studentnummer og studentnavnene henger sammen.
I små eksempler som her er det som regel ikke krise å velge feil datatype. I større prosjekter har feil valg av datatyper en stygg tendens til å skape mange små problemer. Det er derfor viktig at du kjenner til flere sammensatte datatyper enn bare lister. I slutten av dette kapittelet vil du ikke lenger være en toåring med hammer. Du vil være en særdeles skarp toåring med en solid verktøykasse. Tenk på så mye ugagn du kan gjøre da!
9.2 Lås informasjonen med tupler
Den første nye datatypen vi skal se på er den som ligner mest på lister. Dette er tupler75. Her kan du se hvordan vi kan opprette og hente ut elementer fra en tuppel:
# Oppretter en tuppel med gudinner
gudinner = ('Afrodite', 'Hestia', 'Persefone')
# Skriver ut den første gudinnen
print(gudinner[0])Som du kan se av koden ovenfor er tupler laget veldig likt som lister, bare at vi bruker parentesene ( og ) fremfor klammeparentesene [ og ]. Vi kan på samme måte som med lister hente ut elementer ved å bruke indeksering. Tupler har derfor en ordnet rekkefølge på elementene sine.
Tupler støtter både slicing og negativ indeksering akkurat som lister. Du kan også lagre forskjellige datatyper i tupler akkurat som lister. Hva er da egentlig forskjellen på tupler og lister?
Den store forskjellen er at du ikke kan endre verdiene i en tuppel. Prøv å kjøre koden under:
# Oppretter en tuppel med gudinner
gudinner = ('Afrodite', 'Hestia', 'Persefone')
# Prøver å endre den første gudinnen
gudinner[0] = 'Ishtar'Kjører du koden over vil du få en TypeError med en feilmelding som dette:
TypeError: ‘tuple’ object does not support item assignment.
Dette forklarer oss at tupler ikke støtter å endre verdiene slik som lister. Vi sier at lister kan endres76, mens tupler kan ikke endres77. Dette betyr at tupler er bedre egnet der du vet at innholdet forblir uendret. Her er to eksempler der vi er temmelig sikre på at innholdet ikke skal endre seg:
ukedager = (
'Mandag',
'Tirsdag',
'Onsdag',
'Torsdag',
'Fredag',
'Lørdag',
'Søndag'
)
sommerbyer_ol_mellom_2000_og_2024 = (
'Sydney', 'Athen', 'Beijing',
'London', 'Rio de Janeiro',
'Tokyo', 'Paris'
)Både ukedager og sommerbyer_ol_mellom_2000_og_2024 er tupler og du kan dermed ikke endre verdiene i dem. Dette gjør det trygt å sende slike tupler inn i ukjente funksjoner som andre har skrevet. Du kan være sikker på at ingen endrer verdiene i dem uten at du legger merke til det.
Vær oppmerksom på at du må inkludere et komma når du lager en tuppel med ett element:
Hvorfor må du ha et komma her? Dette er for at Python ikke skal misforstå og tenke at du mener parenteser som vi bruker for å skille regneoperasjoner. Skriver du
verdi = ((5 + 3) * 2)
så hadde det vært dumt om dette ble tolket som en tuppel bare fordi de ytterste parentesene ikke egentlig er nødvendige.
9.2.1 Tupler fra returverdier
Om du velger å selv bruke tupler eller ikke så vil du støte på dem uansett. Funksjoner som returnerer mer enn en verdi returnerer nemlig en tuppel:
def utregning_skatt(brutto_lønn, skattesats):
"""Regner ut lønn etter skatt (nettolønn).
Returnerer både nettolønn og skattetrekk"""
netto_lønn = brutto_lønn * (1 - skattesats/100)
skattetrekk = brutto_lønn - netto_lønn
return netto_lønn, skattetrekk
lønn_bjørnar = 593_000
verdi = utregning_skatt(lønn_bjørnar, 25)
print(type(verdi))Her ser du at vi får tilbake en tuppel når vi kjører koden. Vi kan dermed hente ut de forskjellige verdiene:
lønn_bjørnar = 593_000
verdi = utregning_skatt(lønn_bjørnar, 25)
netto_lønn_bjørnar = verdi[0]
print(f'Nettolønn til Bjørnar: {round(netto_lønn_bjørnar)} kroner.')
skattetrekk_bjørnar = verdi[1]
print(f'Skattetrekk til Bjørnar: {round(skattetrekk_bjørnar)} kroner')Slik er det fullt mulig å gjøre det. Likevel er den vanligste måten å direkte hente ut verdiene uten et mellomsteg:
lønn_bjørnar = 593_000
netto_lønn_bjørnar, skattetrekk_bjørnar = utregning_skatt(lønn_bjørnar, 25)
print(f'Nettolønn til Bjørnar: {round(netto_lønn_bjørnar)} kroner.')
print(f'Skattetrekk til Bjørnar: {round(skattetrekk_bjørnar)} kroner')Koden over gjør akkurat det samme som tidligere, bare at vi henter ut de individuelle elementene direkte fremfor å lagre tuppelen i en variabel. Det er likevel nyttig å forstå at funksjoner med flere returverdier returnerer egentlig en tuppel som vi henter verdier ut av. Hvis du ikke vet dette kan du skrive følgende unødvendige kode:
lønn_bjørnar = 593_000
netto_lønn_bjørnar, skattetrekk_bjørnar = utregning_skatt(lønn_bjørnar, 25)
både_netto_og_skattetrekk = (netto_lønn_bjørnar, skattetrekk_bjørnar)Her går vi litt i ring. Funksjonen utregning_skatt() returnerer en tuppel med de to ønskede verdiene allerede. Vi pakker dem først ut i individuelle variabler og så setter vi dem sammen igjen til en tuppel. Vi kan gjøre livet vårt enklere ved å bare skrive:
9.2.2 Minnebruk
I tillegg til at tupler ikke kan endres bruker tupler ofte mindre minne enn lister. Koden under bruker pakken sys i standardbiblioteket til å regne ut minnebruken til en liste og en tuppel som inneholder ti millioner tall:
from sys import getsizeof
# Bygg en liste med 10 millioner tall
stor_liste = []
for tall in range(10_000_000):
stor_liste.append(tall)
print(f'Størrelsen på listen i bytes: {getsizeof(stor_liste)}')
stor_tuppel = tuple(stor_liste)
print(f'Størrelsen på tuppelen i bytes: {getsizeof(stor_tuppel)}')Hvis du lurer på hva en byte er, så kan kan vi se tilbake på maskinkoden vi undersøkte i seksjon 1.1:
\[01100110 \,\, 110101001 \,\, 010100010 \,\, 11110010\] Informasjonen som blir lagret i et enkelt tegn kalles en bit78. Siden hver bit er uavhengig fra den neste så er det \(2^8 = 256\) forskjellige konfigurasjoner for hver gruppe med 8 bits. Vi kaller informasjonen i en slik gruppe med 8 bits for en byte79. Instruksjonen ovenfor består dermed av 4 bytes.
La oss nå forklare koden vi skrev: Koden bruker funksjonen getsizeof() fra standardbiblioteket sys til å regne ut minnebruken til både listen og tuppelen. Vi omgjør listen vi lager til en tuppel med å bruke den innebygde funksjonen tuple(). Kjører jeg koden over får jeg at listen bruker 89095160 bytes minne, altså ca. 89 megabytes. Etter å ha gjort dette om til en tuppel får jeg at tuppelen bruker 80000040 bytes minne, altså ca. 80 megabytes. Her ser vi at vi får en besparelse på nesten 10 %.
Selv om minnebesparelser er kult er det ikke alltid like relevant. Jobber du ikke med store datamengder eller systemer som krever høy ytelse er minnebruk mindre viktig. Så det at tupler har mindre fotavtrykk enn lister på minnebruk er ikke primærgrunnen til å bruke tupler. Se på det heller som en gratis bonus.
Jeg har fremlagt tupler som nærmest bedre enn lister. Det er viktig at du forstår at tupler ikke alltid kan erstatte lister. Hvis du trenger å endre elementene i løpet av programmet, kan du ikke bruke tupler like enkelt. Spillet tre på rad som vi lagde i kapittel 7 brukte lister til å representere brettet. Det hadde vært et ganske kjedelig spill hvis brettet aldri endret seg. Da kunne ingen av spillere gjort noe som helst!
9.3 Fjern duplikater med mengder
En datatype som ofte er veldig nyttig for å fjerne repeterte verdier er mengder80. En mengde er en samling med elementer som er:
Uordnet: Mengder har ikke en fast rekkefølge på elementene. Dette er forskjellig fra lister og tupler siden de har en innebygd rekkefølge.
Unike: Alle elementene i en mengde er unike. Dette er forskjellig fra lister og tupler siden de kan ha flere repeterte verdier.
La oss starte med å lære hvordan vi jobber med mengder i Python. Etterpå skal vi se på noen eksempler der mengder er veldig hjelpsomt. Her er det bare å glede seg, for det er mange kule triks du kan bruke mengder til. De fleste som skriver Python bruker mengder altfor lite, så her kan du fort imponere ekspertene.
9.3.1 Opprettelse av mengder
For å lage en mengde fra bånn av kan du skrive følgende:
Som du ser, bruker vi krøllparenteser { og } for å opprette mengder. Prøver du å lage en mengde med repeterte elementer så vil repetisjonene bare bli fjernet:
favorittfag = {'Biologi', 'Sosiologi', 'Fransk', 'Biologi'}
print(len(favorittfag))
print(favorittfag)Den nå velkjente funksjonen len() fungerer også tipp topp på mengder. Som du kan se når du kjører koden over så har mengden favorittfag bare 3 elementer. Dette er fordi bare én av strengene 'Biologi' blir med i favorittfag.
Ofte vil du ikke bygge en mengde fra bånn, men heller konvertere en liste eller en tuppel til en mengde. Da bruker vi bare funksjonen set() slik:
Det er ikke så intuitivt hvordan man lager en mengde uten noen elementer. Du ville kanskje gjettet på at det var tom_mengde = {}? Dette stemmer ikke, siden ordbøker som vi skal lære om i seksjon 9.4 har beslaglagt denne notasjonen for å lage en tom ordbok. Du kan bruke funksjonen set() for å lage en tom mengde:
Du kan også bruke funksjonene list() og tuple() til å lage tomme lister og tupler. Her bruker vi likevel typisk [] og () siden dette er litt enklere å lese.
Dette bringer oss til det første trikset du kan bruke mengder til. Sett at du har en liste der du ønsker å fjerne alle repeterte elementer. Her har du to måter å gjøre det på:
redskaper = ['Kniv', 'Ostehøvel', 'Vinglass', 'Kniv', 'Visp']
# Metode 1: For-løkker
unike_redskaper_metode1 = []
for redskap in redskaper:
if redskap not in unike_redskaper_metode1:
unike_redskaper_metode1.append(redskap)
# Metode 2: Mengder
unike_redskaper_metode2 = list(set(redskaper))I den første metoden oppretter vi en ny liste som heter unike_redskaper_metode1. Deretter legger vi til elementer fra redskaper til unike_redskaper_metode1 hvis de ikke er der fra før av. Dette går helt fint å skrive, men det er likevel fire linjer med kode for å gjøre noe relativt enkelt.
I den andre metoden bruker vi mengder. Her transformerer vi først listen redskaper til en mengde med funksjonen set() slik at repetisjoner blir fjernet. Deretter bruker vi funksjonen list() til å transformere dette til listen unike_redskaper_metode2. Dette passer oversiktlig på en enkel linje med kode!
Her ser vi at mengder ofte kan forkorte antall linjer med kode du må skrive. Likevel er det et lite offer vi må gjøre. Når vi bruker den første metoden med for-løkker er vi alltid sikre på at rekkefølgen på unike_redskaper_metode1 vil være lik som rekkefølgen i redskaper. Dette er ikke sant når vi bruker den andre metoden med mengder. Mengder er som tidligere nevnt uordnet, så de har ingen rekkefølge. Siden dette er et mellomsteg i den andre metoden vil ikke listen unike_redskaper_metode2 nødvendigvis få samme rekkefølge som listen redskaper.
9.3.2 Operasjoner på mengder
La oss gå gjennom de viktigste metodene og operasjonene knyttet til mengder. Dette vil gjøre deg bedre til å bruke mengder effektivt når du programmerer.
For å legge til flere elementer til en mengde kan du bruke metoden .add(). Denne metoden legger til et element til en mengde så sant elementet ikke allerede eksisterer i mengden:
ledige_togseter = {'D78', 'C33', 'C34', 'B24', 'A47'}
ledige_togseter.add('D13')
ledige_togseter.add('C33')
print(len(ledige_togseter))Som du kan se av koden over så vil lengden til ledige_togseter bare endres første gang du bruker metoden .add(). Dette er fordi den andre gangen du bruker metoden add() så eksisterer allerede elementet 'C33' i mengden. Siden mengder ikke har noen repeterende elementer endres derfor ingenting.
Du kan fjerne elementer med metoden .remove(). Lag deg en mengde selv og prøv å legge til og trekke fra et par elementer for å bli komfortabel med dette. Mengder har også metoden .clear() som fjerner alle elementene.
Du kan derimot ikke endre elementene i en mengde på noen måte. I en slik situasjon blir du nødt til å først fjerne elementet med metoden .remove(), og deretter legge til den ønskede endringen med metoden .add().
Gitt to mengder A og B er det er tre operasjoner som er mye brukt:
Union: Du kan skrive
A | BellerA.union(B)for å ta unionen av to mengder. Dette velger ut alle elementene i mengdeAog mengdeB.Snitt: Du kan skrive
A & BellerA.intersection(B)for å ta snittet av to mengder. Dette velger bare ut elementene som er i både mengdeAog mengdeB.Differanse: Du kan skrive
A - BellerA.difference(B)for å ta differansen av to mengder. Dette velger ut elementer i mengdeAsom ikke er med i mengdeB.
Operasjonene ovenfor er nyttige i forskjellige situasjoner. Her ser du et kodeeksempel der både union og differanse kommer til unnsetning:
ansatte_start_år = {
'Børre Bjørnsen',
'Mikal Meyer',
'Hilde Hestmo',
'Johanne Jensen'
}
nyansettelser = {'Frida Fredriksen', 'Linda Løren'}
oppsigelser = {'Johanne Jensen', 'Linda Løren'}
ansatte_slutt_år = (ansatte_start_år | nyansettelser) - oppsigelserHer danner vi mengden ansatte_slutt_år ved å først samle sammen alt av eksisterende ansatte ved starten av året sammen med nyansettelser. Deretter fjerner vi navnene på dem som har sagt opp i løpet av året. Her er koden både leselig og kort. Skriver du samme logikk ved å bruke for-løkker vil dette bli litt mer omfattende.
9.3.3 Mengder inni andre mengder
Vi har i seksjon 4.6 jobbet med lister inni andre lister. Oppgave 2 i slutten av dette kapittelet gir deg også muligheten til å jobbe med tupler inni andre tupler. Hva med mengder inni andre mengder? Det vil kanskje overraske deg at du ikke kan lage mengder inni andre mengder:
Kjører du koden over vil du få en TypeError som ligner på denne:
TypeError: unhashable type: ‘set’
Dette forteller oss at det ikke er alle elementer i Python som kan legges inn i mengder. Elementer i mengder må opprettholde en egenskap som på engelsk heter hashable. Det finnes ikke et veletablert norsk begrep for dette, men hashbarhet fungerer til nøds. Mengder i seg selv er ikke hashbar, så du kan derfor ikke legge mengder inn i andre mengder. Et annet eksempel på en datatype som ikke er hashbar er lister. Følgende kode vil også gi samme feil:
Nå er det lett å tenke at det ikke er mulig å legge inn sammensatte datatyper inni mengder. Prøver du å legge en tuppel inni en mengde derimot går dette tilsynelatende helt tipp topp!
Vi skal ikke gå inn på nøyaktig hva hashbar betyr i denne boken. Konseptet har med hvordan mengder strukturerer informasjonen sin. Denne struktureringen gjør at det er veldig raskt å sjekke om et element er med i en mengde med å bruke nøkkelordet in. For at dette skal være mulig gjør Python noen restriksjoner på hva mengder kan inneholde.
Heldigvis for deg er det ikke så vanlig at du trenger å legge mengder inni andre mengder. Hvis du likevel trenger dette har Python en datatype som heter en fryst mengde81. Dette skal vi ikke gå inn på, men du kan søke opp dette selv hvis du er interessert.
9.4 Raskt oppslag med ordbøker
Vi har sett at det er forskjeller mellom lister, tupler, og mengder. Både lister og tupler er ordnet, mens mengder har ingen rekkefølge på elementene. Både lister og mengder kan endres, mens det ikke går an å endre en tuppel. Likevel har alle tre datatypene litt samme filosofi. De inneholder en samling med elementer som vanligvis er logisk relatert til hverandre. Nå skal vi lære om ordbøker82 som er grunnleggende forskjellig fra dette.
Ordbøker parer opp to biter med informasjon. Her er tre eksempler:
Paring mellom navn og telefonnummer i en telefonkatalog. Hvis du er for ung til å relatere til dette så kan du tenke på tjenester som 1881.
Paringen mellom navnet på bøker og sidetallet til bøkene.
Paringen mellom brukere på en nettside og antall sekunder de har oppholdt seg på nettsiden.
I stedet for å ha en enkel samling med elementer, har vi her en annen filosofi. Biter med informasjon kommer i par. Tenkt paringen mellom bøker og sidetall. Hver bok har et sidetall, så de to bitene med informasjon henger sammen. Likevel har for eksemepl boken '21 tanker for det 21 århundret' lite å gjøre med antall sider i boken 'Antikken på trikken'. All informasjon i en ordbok er altså ikke nødvendigvis logisk relatert til hverandre.
Vi skal nå gå gjennom alt fra oppretting av ordbøker, endring av elementer, metoder på ordbøker, for-løkker knyttet til ordbøker, og til slutt ordbøker inni andre ordbøker. Vi bruker mye tid på dette siden ordbøker er viktig å mestre i Python. Det er sjelden jeg ser større programmer som ikke bruker ordbøker, og i de få tilfellene rynker jeg som regel på nesen for meg selv.
9.4.1 Oppretting av ordbøker
La oss starte med å opprette eksemplene på ordbøker vi nevnte tidligere. Etterpå kan vi bruke disse eksemplene som en god basis for videre diskusjon:
# Kan skrive en ordbok på en linje hvis den er kort nok
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
# Ellers skriver vi ordbøkene typisk slik
bøker_sidetall = {
'21 tanker for det 21 århundret': 367,
'Antikken på trikken': 176,
'Opplev Norge': 224
}
besøkstid_nettside = {
173937: 23.5,
173938: 222.1,
173939: 118.9,
173910: 117.3
}Som du kan se bruker vi krøllparentesene { og } for å lage ordbøker, akkurat som for å lage mengder. Forskjellen er at ordbøker har flere par som har formen
nøkkel1: verdi1, nøkkel2: verdi2, ....
Her er forholdet at nøkkel1 peker til verdi1, nøkkel2 peker til verdi2, osv. I eksempelet telefonkatalog over kaller vi dermed navnet 'Hans Hansen' og navnet 'Lise Andersen' nøkler83, mens telefonnumrene '90382946' og '90382990' kalles for verdier84.
Som du kan se i ordboken bøker_sidetall så kan også heltall være verdier i en ordbok. I ordboken besøkstid_nettside ble brukerne på siden representert med et heltall som gir en unik indikator, mens besøkstiden på siden er gitt i sekunder som flyttall. Dette viser at heltall kan være nøkler, mens flyttall kan være verdier i en ordbok. Kan alle datatyper være nøkler og verdi i ordbøker? Svaret er at alt kan være verdier, mens nøkler må være hashbare. Dette er igjen egenskapen som vi møtte på når vi snakker om mengder. Følgende ordbok er ikke gyldig:
Prøver du deg med koden ovenfor vil du få en TypeError med feilmeldingen
unhashable type: ‘list’.
Her er igjen problemet at lister ikke er hashbare, så de kan dermed ikke brukes som nøkler i ordbøker.
Siden nøkler i en ordbok peker til en unik verdi kan du ikke ha flere like nøkler i en ordbok. Prøver du deg med dette vil du bare overskrive nøkkelen:
bøker_sidetall = {
'21 tanker for det 21 århundret': 367,
'Antikken på trikken': 176,
'Opplev Norge': 224,
'Antikken på trikken': 189
}
print(bøker_sidetall)Du vil bare ha en nøkkel som heter 'Antikken på trikken'. Verdien som hører til nøkkelen 'Antikken på trikken' vil nå være 189. I motsetning til nøkler går det helt fint å flere like verdier som passer til flere nøkler. For eksempel kan flere ulike bøker ha samme antall sider.
En ordbok kan inneholde forskjellige datatyper. Spesielt vanlig er det med forskjellige datatyper for verdiene, mens nøklene ofte er strenger. Her er en ordbok som representerer salgsinnformasjon til en laptop:
laptop = {
'prosessor': 'Core i7',
'RAM (GB)': 32,
'skjerm (tommer)': 15.6,
'farger': ['Svart', 'Hvit', 'Grå']
}Ved å lagre informasjon i ordboken laptop fremfor i spredte enkeltvariabler er sammenhengen klarere.
Før vi går videre er det greit å vite at vi kan lage den tomme ordboken ved å skrive tom_ordbok = {}. I tillegg kan du bruke funksjonen len() til å sjekke hvor mange par med nøkler og verdier en ordbok har. Ingen overraskelser her!
9.4.2 Henting og endring av verdier i ordbøker
Når vi har en ordbok er det ønskelig å hente ut verdier som hører til en gitt nøkkel. Dette kan du bruke til å finne nummeret til et navn i en telefonkatalog, eller finne ut hvor lenge en bruker har vært på en nettside. For dette brukes vi klammeparentesene [ og ] slik:
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
nummer = telefonkatalog['Hans Hansen']
print(f'Nummeret til Hans Hansen er {nummer}.')
besøkstid_nettside = {
173937: 23.5,
173938: 222.1,
173939: 118.9,
173910: 117.3
}
bruker_id = 173939
tid = besøkstid_nettside[bruker_id]
print(f'Besøkstiden til brukeren med id {bruker_id} er {tid} sekunder.')I det første eksempelet med telefonkatalog brukte jeg strengen 'Hans Hansen' direkte når jeg hentet telefonnummeret. I det andre eksempelet med besøkstid_nettside lagde jeg en egen variabel bruker_id som holdt heltallet som representerer brukeren sin identifikasjon. Deretter brukte jeg dette til å hente ut besøkstiden til den brukeren. Begge deler går helt fint og brukes basert på hva som er mest beleilig.
Hva skjer når du prøver å hente en nøkkel som ikke eksisterer i en ordbok? Da får du problemer som følgende kode viser:
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
nummer = telefonkatalog['Bob Karlsen']
print(f'Nummeret til Bob Karlsen er {nummer}.')Kjører du koden over vil du få en KeyError. Dette er en ny feil vi ikke har sett tidligere. Får du en KeyError betyr det at du refererer til en nøkkel som ikke eksisterer. Vi kan bruke feilhåndtering som vi skal lære i kapittel 12 til å håndtere KeyError når vi prøver å hente nøkler som ikke eksisterer fra en ordbok. Heldigvis for oss er dette problemet så vanlig at Python har en egen metode som redder oss.
Ordbøker har metoden .get() som ved første øyekast ser ut som den virker helt likt som å hente elementer med klammeparentesene [ og ]:
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
nummer = telefonkatalog.get('Hans Hansen')
print(f'Nummeret til Hans Hansen er {nummer}.')Så lenge nøkkelen vi søker etter er i ordboken så gir dette oss akkurat det samme. Hvis nøkkelen ikke eksisterer så produserer ikke metoden .get() en KeyError. Metoden .get() returnerer heller verdien None. Så her kan vi skrive en enkel if-else setning for å håndtere dette:
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
navn = 'Karl Karlsen'
nummer = telefonkatalog.get(navn)
if nummer:
print(f'Nummeret til {navn} er {nummer}.')
else:
print(f'Nummeret til {navn} eksisterer ikke i telefonkatalogen.')Situasjonen der du ikke vet på forhånd om en nøkkel er med i en ordbok er faktisk ekstremt vanlig. Variabelen navn i koden over kunne vært hentet inn med funksjonen input() fra en sluttbruker. I større applikasjoner som 1881 vet man heller ikke nøyaktig hva en sluttbruker vil søke på før de gjør det. Derfor er metoden .get() svært nyttig.
Det er ingenting i veien for å endre verdien som korresponderer til en nøkkel. Du kan også lage et helt nytt par med en nøkkel og en verdi. Koden under viser hvordan du gjør dette:
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
# Hans Hansen har fått et nytt telefonnummer
telefonkatalog['Hans Hansen'] = '895422344'
# En ny person registrerer seg
telefonkatalog['Kåre Kålrabi'] = '88557320'Du kan fjerne en nøkkel og den tilhørende verdien ved å bruke nøkkelordet del:
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
# Hans Hansen melder seg ut av telefonkatalogen
del telefonkatalog['Hans Hansen']En vanlig situasjon du kanskje har opplevd, er at du prøver å registrere deg på en nettside, men får beskjed om at du allerede har en konto. Er ikke alltid lett å huske hva man har registrert seg på tidligere. Her kan nettsiden først bruke .get() til å sjekke om du allerede har registrert deg tidligere. Nettsiden bør bare lage en ny bruker til deg hvis du ikke finnes i systemene deres fra før av:
brukere = {
'hans.hansen@gmail.com': '89584739',
'lise.andersen@hotmail.com': '58940394'
}
epost = input('Hva er eposten din? ')
if brukere.get(epost):
print(f'Ser ut som du har en bruker fra før av. Logg inn med {epost}.')
else:
telefonnummer = input('Hva er telefonnummeret ditt? ')
brukere[epost] = telefonnummer
print('Da har vi opprettet en ny bruker!')9.4.3 For-hver løkker og ordbøker
Både tupler, mengder, og ordbøker støtter for-hver løkker slik som lister. For mengder og tupler fungerer det akkurat slik du skulle tro:
gudinner = ('Afrodite', 'Hestia', 'Persefone')
for gudinne in gudinner:
print(gudinne)
byer = {'Oslo', 'Bergen', 'Trondheim'}
for by in byer:
print(by)Hva med for ordbøker? Tenk litt på hva du tror følgende kode skriver ut til terminalen før du kjører den selv:
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
for person in telefonkatalog:
print(person)Som du ser når du kjører koden så blir nøklene 'Hans Hansen' og 'Lise Andersen' skrevet ut til terminalen. Standardoppførselen når vi bruker for-hver løkker sammen med ordbøker er dermed at nøklene blir brukt. Du kan få tak i verdiene ved å hente dem:
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
for person in telefonkatalog:
telefonnummer = telefonkatalog[person]
print(f'Nøkkelen {person} har verdi {telefonnummer}.')Så du har alltid tilgjengelig både nøkler og verdier om du trenger begge to. Hvis du bare ønsker verdiene så kan du også bruke metoden .values():
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
for telefonnummer in telefonkatalog.values():
print(f'Verdien er {telefonnummer}.')Dette sparer deg fra å først måtte bruke nøklene til å hente verdiene. Hvis du har behov for å bruke både nøklene og verdiene, kan du også bruke metoden .items():
telefonkatalog = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
for person, telefonnummer in telefonkatalog.items():
print(f'Nøkkelen {person} har verdi {telefonnummer}.')Her synes jeg vi bør pause litt for å forstå notasjonen person, telefonnummer. Hva er det egentlig som skjer her? Koden telefonkatalog.items() returnerer en listelignende datatype. La oss se hvordan denne ser ut med å omgjøre det til en liste:
print(list(telefonkatalog.items()))
>>> [('Hans Hansen', '90382946'), ('Lise Andersen', '90382990')]Dette er en liste med tupler i seg. Så for iterasjonene i for-løkken vil vi dermed ha følgende verdier:
# Første iterasjon
person, telefonnummer = ('Hans Hansen', '90382946')
# Andre iterasjon
person, telefonnummer = ('Lise Andersen', '90382990')Dette har vi sett er en måte å pakke ut tupler på tidligere. Så i første iterasjon vil person ha verdien 'Hans Hansen' og telefonnummer vil ha verdien '90382946'. I den andre iterasjonen vil person ha verdien 'Lise Andersen' og telefonnummer vil ha verdien '90382990'. Så notasjonen gir mening når vi undersøker litt.
Til slutt bør du vite om en endring fra og med Python versjon 3.7. For versjoner av Python høyere eller lik 3.7 har ordbøker bevart rekkefølgen nøkler og korresponderende verdier ble satt inn i ordboken. Før Python 3.7 var denne rekkefølgen ikke noe du kunne stole på. Så når vi bruker for-hver løkker kan du være sikker på at rekkefølgen du får nøkler og korresponderende verdier er den samme som du satte dem inn i ordboken.
Det er vanlig å høre folk si at ordbøker fra og med Python versjon 3.7 har en rekkefølge. Dette er litt misvisende: Når du sammenligner to ordbøker for likhet så bryr ikke Python seg om rekkefølgen på nøkler og korresponderende verdier. Dette er i kontrast fra både lister og tupler, der rekkefølge er helt sentralt for om to objekter er like eller ikke:
telefonkatalog1 = {'Hans Hansen': '90382946', 'Lise Andersen': '90382990'}
telefonkatalog2 = {'Lise Andersen': '90382990', 'Hans Hansen': '90382946'}
print(f'De er sett på som like: {telefonkatalog1 == telefonkatalog2}')
gudinner1 = ('Afrodite', 'Hestia', 'Persefone')
gudinner2 = ('Persefone', 'Hestia', 'Afrodite')
print(f'De er sett på som like: {gudinner1 == gudinner2}')Hvis du virkelig vil ha ordbøker som bryr seg fullt og helt om rekkefølge kan du finne denne datatypen i standardbiblioteket collections. Der har man nemlig en datatype som heter OrderedDict. Neste gang du hører noen si på en fest at ordbøker i Python er ordnet har du min velsignelse til å starte en tirade.
9.4.4 Ordbøker inni andre ordbøker
Vi nevnte i seksjon 9.3 at det ikke er mulig å legge mengder inni andre mengder grunnet mangelen på egenskapen hashbar. For ordbøker er det ikke bare mulig, men også utrolig nyttig å ha dem inni hverandre. La oss se hvordan dette fungerer. Akkurat som med lister inni lister i seksjon 4.6 vil du ikke egentlig lære noe nytt her. Likevel er det veldig viktig å få trening med dette siden det er så mye brukt.
Sett at vi ønsker å representere informasjon om noen av karakterene i serien Frustrerte fruer. En måte å gjøre dette på er ved å lage ordbøker inni ordbøker slik:
frustrerte_fruer = {
'susan': {
'fornavn': 'Susan',
'etternavn': 'Mayer',
'alder': 40
},
'lynette': {
'fornavn': 'Lynette',
'etternavn': 'Scavo',
'alder': 42
},
'gabrielle': {
'fornavn': 'Gabrielle',
'etternavn': 'Solis',
'alder': 29
}
}Hvis vi ser på den ytterste ordboken frustrerte_fruer så har den nøklene 'susan', 'lynette', og 'gabrielle'. Verdiene som disse tre nøklene peker mot er de indre ordbøkene med informasjon om hver av karakterene.
La oss begynne med å hente ut etternavnet til Lynette. Vi må første hente ut henne som person fra ordboken frustrerte_fruer, og deretter hente ut etternavnet hennes. Vi kan enten gjøre dette i to separerte steg, eller alt samtidig om vi føler oss modige nok:
# Gjøre det i to separate steg
lynette = frustrerte_fruer['lynette']
lynette_etternavn = lynette['etternavn']
# Gjøre alt i ett steg
lynette_etternavn = frustrerte_fruer['lynette']['etternavn']Her er det en typisk avveining du må gjøre. Å hente det i et enkelt steg gir færre linjer med kode, men den ene linjen med kode blir også litt vanskeligere å lese. Hvis du skal hente mange nivåer innover så ville jeg nok delt det opp. Her derimot synes jeg det går helt fint å gjøre alt i ett steg uten at vi mister særlig leselighet.
Etter hvert kan du prøve å besvare mer kompliserte spørsmål angående informasjonen du har. For eksempel, hva er gjennomsnittsalderen på de tre kvinnene i frustrerte_fruer? Her må vi hente ut alderen til hver av dem og ta gjennomsnittet. Dette kan vi gjøre med en for-hver løkke med metoden .items() slik:
gjennomsnittsalder = 0
for frue in frustrerte_fruer.values():
gjennomsnittsalder += frue['alder']
gjennomsnittsalder = gjennomsnittsalder / len(frustrerte_fruer)
print(f'Gjennomsnittsalderen er {round(gjennomsnittsalder, 1)} år.')Jeg synes selv ordbøker inni andre ordbøker var litt utfordrende i begynnelsen. Hvis du kjenner deg igjen i dette så bruk gjerne litt tid på å trene på ordbøker inni andre ordbøker. Start med enkle eksempler der du bare skal hente ut en bit med informasjon. Gradvis kan du lage vanskeligere oppgaver til deg selv.
9.5 Listebeskrivelser
Så langt har du sett hvordan å opprette lister, tupler, mengder, og ordbøker. I Python finnes det en alternativ måte å opprette disse sammensatte datatypene som heter beskrivelser85. Denne alternative måten er veldig elegant og brukes ofte når profesjonelle skriver Python. I denne seksjonen skal vi se på hvordan vi kan bruke beskrivelser til å opprette lister. I neste seksjon tar vi for oss andre beskrivelser.
Når vi snakker om beskrivelser for å bygge lister kalles dette naturlig nok for listebeskrivelser86. Motivasjonen til listebeskrivelser er at opprettelse av nye lister basert på tidligere lister kan kreve mye kode. Her har du en rimelig standard situasjon:
handlekurv = ['saft', 'havremelk', 'appelsin', 'epler', 'Donald Duck blad']
korte_navn = []
for vare in handlekurv:
if len(vare) <= 5:
korte_navn.append(vare)Her tar det 4 linjer med kode å lage den nye listen korte_navn med varer som har korte navn. Dette er ganske mye arbeid for noe som er temmelig enkelt. Er det mulig å gjøre dette uten så mange linjer med kode? Her kommer listebeskrivelser til god nytte:
handlekurv = ['saft', 'epler', 'havremelk', 'appelsin', 'Donald Duck blad']
korte_navn = [vare for vare in handlekurv if len(vare) <= 5]Hvis du leser den nye linjen over, legg merke til hvor leselig den er. Vi lager en ny liste korte_navn som gir oss varene i handlekurven som har lengde mindre eller lik fem. Listebeskrivelser er en kompakt måte å lage nye lister på. Et annet klassisk eksempel er å lage alle kvadrattallene fra 1 til 10 slik:
Sett at du har en situasjon som passer inn i dette mønsteret:
Da kan du skrive dette som en ekvivalent listebeskrivelse slik:
Her bruker vi stedfortrederen betingelse som en sannhetsverdi avhengig av element. Stedfortrederen modifisert_element betegner en mulig modifikasjon av element i den gamle samlingen gammel_samling. Et eksempel på modifisert_element så vi tidligere med tall ** 2 når vi lagde kvadrattallene. Det er verdt å merke seg at:
En listebeskrivelse ikke trenger å ha en if-setning. En if-setning var til stede for eksempelet med varer i en handlekurv, men ikke når vi lagde kvadrattallene.
Samlingen
gammel_samlingtrenger ikke nødvendigvis være en liste. I eksempelet med kvadrattall kom dette fra funksjonenrange(). Det kan også for eksempel være en tuppel.Listebeskrivelser er ikke et substitutt for alle for-hver løkker som jobber med lister. Det kan bare substituere for-hver løkker som lager nye lister basert på tidligere lister. Leser du over den endelige koden fra tre på rad prosjektet i kapittel 7 så vil du se at noen av for-hver løkkene bygger nye lister, mens andre har helt andre oppgaver.
Her er en liten advarsel: Det er ikke alltid at du bør bruke en listebeskrivelse bare fordi du kan. Husk at målet med listebeskrivelser er at koden skal bli kortere og mer leselig. Kortere blir den som regel alltid, men noen ganger kan leseligheten lide. Et eksempel er å omskrive følgende kode:
heltall_under_15 = range(1, 15)
spesielle_tall = []
for tall in heltall_under_15:
if tall >= 5 and tall % 2 == 0:
spesielle_tall.append(tall**3 + 15 * tall**2 + 17 * tall + 13)Å omskrive koden over ved å bruke en listebeskrivelse gjør den mindre leselig:
heltall_under_15 = range(1, 15)
spesielle_tall = [tall**3 + 15 * tall**2 + 17 * tall + 13
for tall in heltall_under_15 if tall >= 5 and tall % 2 == 0]For de fleste av oss er versjonen med listebeskrivelsen vanskeligere å lese. Dette er fordi vi gjør veldig mye på en gang. Her ville jeg foretrukket å gjøre det med en eksplisitt for-hver løkke fordi man da kan analysere hver linje i eget tempo. Dette er subjektivt, og det er like mange meninger om dette som det er Python-utviklere. Likevel skal kode som regel leses flere ganger av forskjellige personer. Dermed er å prioritere leselighet fremfor å være kortfattet ofte en god beslutning.
9.6 Andre beskrivelser
Nå som vi har sett listebeskrivelser kan vi utvide dette til å raskt opprette mengder, ordbøker, og tupler. Vi kommer til å gjøre tupler til slutt siden tuppelbeskrivelser ikke egentlig eksisterer. Likevel vil det å forsøke å skrive en tuppelbeskrivelse lede oss til noe mye mer interessant. Vi vil da få vårt første møte med generatorer.
9.6.1 Mengdebeskrivelser
For å lage en mengde ved å bruke listebeskrivelser kunne vi ha brukt funksjonen set() slik:
elver = ['Glomma', 'Otta', 'Otra', 'Namsen', 'Nidelva', 'Otra']
utvalgte_elver = set([elv for elv in elver if elv[0] == 'O'])Her lager vi først en liste med en listebeskrivelse, og deretter bruker vi set() for å gjøre listen om til en mengde. Ulempen her er at vi må opprette en liste som et unødvendig mellomsteg. Heldigvis finnes det en egen notasjon for mengdebeskrivelser87 som lar oss skrive det slik:
elver = ['Glomma', 'Otta', 'Otra', 'Namsen', 'Nidelva', 'Otra']
utvalgte_elver = {elv for elv in elver if elv[0] == 'O'}Notasjonen er kortere, og vi slipper å først opprette en liste som et mellomsteg. Merk at 'Otra' ikke er repetert to ganger i utvalgte_elver siden mengder ikke tillater repetisjoner. Bortsett fra bruken av krøllparentesene { og } istedenfor klammeparentesene [ og ] er mengdebeskrivelser og listebeskrivelser likt skrevet.
9.6.2 Ordbokbeskrivelser
Nå er det ordbøker som står for tur. Som du sikkert har gjettet så er det også en egen notasjon for ordbokbeskrivelser88. Det letteste er å se et par eksempler for å forstå hvordan det fungerer. La oss begynne med å lage en ordbok som representerer kvadrattall som også er partall:
Her ser vi at vi bruker notasjonen {nøkkel: beskrivelse}, der nøkkel er nøkkelen vi ønsker og beskrivelse er som en listebeskrivelse uten klammeparentesene [ og ]. Et vanlig bruksområde til ordbokbeskrivelser er å kombinere to lister i en ordbok:
filmnavn = ['Småkryp', 'Toy Story', 'De Utrolige', 'Innsiden ut']
terningkast = [4, 6, 5, 5]
pixar_filmer = {filmnavn[i]: terningkast[i] for i in range(len(filmnavn))}
print(pixar_filmer)Her kan du se at vi kan pare opp to lister filmnavn og terningkast der filmnavn[i] hører sammen med terningkast[i]. Her er vi helt avhengig av at listene filmnavn og terningkast har like mange elementer i seg. Det er også mulig å gjøre det samme med å bruke den innebygde funksjonen zip() slik:
filmnavn = ['Småkryp', 'Toy Story', 'De Utrolige', 'Innsiden ut']
terningkast = [4, 6, 5, 5]
pixar_filmer = dict(zip(filmnavn, terningkast))
print(pixar_filmer)Funksjonen zip() kombinerer her de to listene filmnavn og terningkast til et spesielt objekt som funksjonen dict() kan gjøre om til en ordbok. Du vil ikke ofte få bruk for funksjonen zip(), men når du først kan bruke den sparer du fort et par linjer med kode.
En god bruk av en ordbokbeskrivelse gir elegant kode. Her har du et eksempel der bruken av metoden .items() gjør det lett å oppdatere prisen på alle varene:
9.6.3 Tuppelbeskrivelser? - Et første møte med generatorer
Basert på hva vi har lært så langt burde det være enkelt å skrive en tuppelbeskrivelse. Vi burde vel bare kunne bytte ut klammeparentesene [ og ] fra listebeskrivelser med parentesene ( og )? La oss prøve og se hvor dette leder:
Kjører du koden over får du ingen feilmeldinger. La oss prøve å skrive ut elementene i kvadrattall med en for-hver løkke:
Kjører vi koden over skriver vi ut alle kvadrattallene mellom 1 og 100. Kan avslutte denne seksjonen med hodet hevet? Her må vi roe oss litt ned, for det finnes nemlig ingen tuppelbeskrivelser i Python. Det vi har for oss her er en generator89. For å se at verden ikke er som vi tror, la oss se på hva kvadrattall egentlig er:
kvadrattall = (tall ** 2 for tall in range(1, 11))
print(f'Typen til kvadradtall er: {type(kvadrattall)}')
print(f'Innholdet til kvadrattall er {kvadrattall}')Når du kjører koden over vil du få noe lignende dette:
Typen til kvadradtall er: <class ‘generator’>
Innholdet til kvadrattall er <generator object
Som du kan se er kvadrattall en generator og ikke en tuppel. Men du får bare noe kryptisk når du prøver å skrive dette ut. Tidligere gikk det helt fint å skrive ut hvert element med en for-hver løkke. Hva er det som foregår?
En generator er et objekt som genererer elementene når du skriver en for-hver løkke, men ikke på forhånd slik som lister, tupler, mengder, og ordbøker. Derfor vil du bare få en tekstforklaring om at du har en generator når du prøver å skrive ut kvadrattall. Verdiene i kvadrattall er nemlig ikke regnet ut ennå. Verdiene blir først regnet ut når du faktisk trenger dem. Dette er veldig effektivt siden du da aldri trenger å lagre alle verdiene, som tar opp plass. For små datamengder har ikke dette særlig mye å si, men for større datamengder kan dette være veldig besparende.
Det vi har skrevet ovenfor er en generatorbeskrivelse90. Akkurat som listebeskrivelser er en måte å lage lister på er generatorbeskrivelser en måte å lage generatorer på. Den andre måten å lage generatorer på bruker nøkkelordet yield i Python. Dette skal vi ikke gå nærmere på i denne boken, men det er likevel greit at du vet om dette.
Når bør du bruke generatorbeskrivelser? Tenkt at du skal finne summen av de første million kvadrattallene:
kvadrattall = (tall ** 2 for tall in range(1, 1_000_001))
sum_kvadrattall = sum(kvadrattall)
print(f'Summen av kvadrattallene opp til 1 million er {sum_kvadrattall}.')Ved å bruke en generatorbeskrivelse trenger du ikke bruke minne på å lagre millioner av tall. Hvert nye tall blir generert når du summer dem opp, så du bruker mindre minne enn om du hadde brukt lister. Du kan også gjøre alt i et steg her uten at det endrer noe:
sum_kvadrattall = sum((tall ** 2 for tall in range(1, 1_000_001)))
print(f'Summen av kvadrattallene opp til 1 million er {sum_kvadrattall}.')Når du direkte sender en generatorbeskrivelse inn i en funksjon får du doble parenteser (( og )) på hver side. Siden dette er ganske vanlig så er det også lov i Python å droppe en av parentesene slik:
sum_kvadrattall = sum(tall ** 2 for tall in range(1, 1_000_001))
print(f'Summen av kvadrattallene opp til 1 million er {sum_kvadrattall}.')På denne måten er generatorbeskrivelser i noen sammenhenger både mer effektive og enklere å lese enn listebeskrivelser siden du slipper klammeparentesene [ og ]. Jeg håper dette var en grei, om noe fortsatt litt mystisk, introduksjon til generatorer.
Så hva med tupler? I Python er det ingen tuppelbeskrivelse. Dette gir jo egentlig mening når du tenker på det: Tupler kan ikke endres, så det burde ikke gå an å gradvis bygge dem opp med å legge til elementer. Beskrivelser er jo tross alt bare fin notasjon på en for-hver for å legge til elementer. Derfor finnes det ikke tuppelbeskrivelser. Du kan likevel gjøre dette hvis du ønsker å bygge en tuppel:
Nå er vi i stand til å forstå hva som skjer i koden over. Beskrivelser som står inne i funksjonen tuple() er en generatorbeskrivelse. Vi bruker den innebygde funksjonen tuple() til å gjøre dette om til en tuppel. Da må hele generatoren bli evaluert, slik at alle elementene blir lagret i minnet. Så du får ikke fordelen med generatorer ved å gjøre dette. Likevel ender du opp med å lage en tuppel på en kort og elegant måte.
Oppsummert er både listebeskrivelser, mengdebeskrivelser, ordbokbeskrivelser, og generatorbeskrivelser veldig nyttig. Du må gjøre et aktivt valg om en beskrivelse løser et problem på en leselig måte, og du må velge hvilken beskrivelse du ønsker å bruke.
9.7 Prosjekt - Quizapplikasjon
I dette prosjektet skal vi lage en kommandolinje-applikasjon for en liten quiz. Vi skal se at riktig bruk av sammensatte datatyper, spesifikt ordbøker og tupler, gjør applikasjonen lett å håndtere. Vi vil også se at ordbøker inni andre ordbøker er ganske naturlig når du skal strukturere informasjon som har flere lag.
9.7.1 Oppgaven
Lag en kommandolinje-applikasjon for en quiz der spilleren får flere spørsmål på rad som har svaralternativer. Til slutt skal spilleren få beskjed om hvor mange poeng spilleren fikk totalt. Det holder med en prototype der vi kun har tre spørsmål i quizen for nå.
9.7.2 Datamodellen
Den grunnleggende logikken i quiz-applikasjonen er veldig lignende programmer vi har sett tidligere. Vi skriver ut et spørsmål til terminalen, ber spilleren gi et svar, sjekker svaret opp mot svaralternativene, regner ut total poengsum, osv. Det vi skal fokusere mer på denne gangen er hvordan vi kan strukturere informasjonen. Dette kalles ofte datamodellen91 til løsningen vår.
En quiz består av flere spørsmål. En mulighet er å representere hvert enkelt spørsmål som en egen variabel. Siden spørsmålene tilhører samme quiz er det likevel greit om de er bundet sammen. Vi velger å lage en overordnet ordbok som heter quiz:
En quiz består av mange spørsmål, så hver nøkkel i ordboken quiz kan være navnet på hvert spørsmål:
For nå la jeg bare inn None som verdier. La oss endre dette. Hvilken informasjon bør høre til et spørsmål? Hvis du tenker litt, vil du sannsynligvis komme frem til følgende:
Spørsmålet.
Svaralternativene.
Spillerens avgitte svar.
Hvilket svar som er riktig.
Poengsum en spiller får for å svare riktig.
Hvilke datatyper bør vi sette til disse bitene med informasjon? En mulighet er at spørsmålet, hvert av svaralternativene, spillerens avgitte svar, og hvilket svar som er riktig alle er strenger. Poengsum kan være et heltall. Men hva med alle svaralternativene som helhet? Skal dette være en liste eller en tuppel? Min tanke er at brukeren ikke skal endre svaralternativene, bare sitt eget svar. Da passer tupler bra siden de ikke kan endres.
La oss legge denne informasjonen inn der vi tidligere bare hadde verdiene None. Siden dette er fem biter med informasjon som henger sammen, så lager vi en ny ordbok slik:
quiz = {
'spørsmål 1': {
'spørsmålstekst': '',
'alternativer': tuple(),
'spiller_svar': '',
'riktig_svar': '',
'poeng': 0
},
'spørsmål 2': {
'spørsmålstekst': '',
'alternativer': tuple(),
'spiller_svar': '',
'riktig_svar': '',
'poeng': 0
},
'spørsmål 3': {
'spørsmålstekst': '',
'alternativer': tuple(),
'spiller_svar': '',
'riktig_svar': '',
'poeng': 0
}
}Som du kan se så har vi bare lagt inn tomme strenger, tomme tupler, og heltallet 0 der informasjonen faktisk skal være. Informasjonen i quiz beskriver nå datamodellen vår. Vi kan legge inn tre spørsmål om fotball for å gjøre ferdig prototypen:
quiz = {
'spørsmål 1': {
'spørsmålstekst': 'Hvem vant toppserien i fotball for kvinner i 2024?',
'alternativer': ('Brann', 'Vålerenga', 'Rosenborg', 'Røa'),
'spiller_svar': '',
'riktig_svar': 'Vålerenga',
'poeng': 10
},
'spørsmål 2': {
'spørsmålstekst': 'Hvilket land vant VM i fotball for menn i 2022?',
'alternativer': ('Argentina', 'Frankrike', 'Tyskland', 'Brasil'),
'spiller_svar': '',
'riktig_svar': 'Argentina',
'poeng': 10
},
'spørsmål 3': {
'spørsmålstekst': 'Hvilket lag vant Champions League for menn i 2023?',
'alternativer': ('Manchester City', 'Liverpool', 'Chelsea', 'Milan'),
'spiller_svar': '',
'riktig_svar': 'Manchester City',
'poeng': 15
}
}Merk at vi ikke har lagt inn noe informasjon som hører til nøkkelen 'spiller_svar'. Denne informasjon vil bli lagt inn når programmet kjører. Derfor er det viktig at ordbøker kan endres, slik at vi kan legge inn ny informasjon dynamisk. Nå er vi klare til å skrive spillets logikk.
9.7.3 Et forenklet spill
La oss som vanlig begynne med å løse et litt forenklet problem. Vi tar først for oss en gjennomgang av quizen uten følgende to komponenter:
Sjekking om spilleren sitt svar er et av svaralternativene
Beregning av poengsum etter avsluttet quiz
Hvorfor utsetter jeg dette? Dette er ekstrafunksjonalitet som gjør applikasjonen vår bedre, men som ikke er strengt tatt nødvendig for en første versjon. I første omgang holder det at vi bygger funksjonaliteten som stiller brukeren spørsmål og lagrer svarene i ordboken quiz. La oss til og med gjøre det enklest mulig først å bare trekke ut et enkelt spørsmål som vi jobber med:
Her skriver vi så langt bare ut spørsmålet. Vi må liste opp alle svaralternativer og deretter be spilleren om å skrive inn sitt svar:
print('\nSvaralternativer:')
for alternativ in spørsmål1['alternativer']:
print(alternativ)
spiller_svar = input('Svar: ')Etter at spilleren har skrevet inn svaret sitt må vi lagre dette svaret i ordboken igjen:
Til slutt må vi sjekke om dette faktisk var riktig svar. Her holder det med en enkel if-else setning:
riktig_svar = spørsmål1['riktig_svar']
if spiller_svar == riktig_svar:
print('Helt riktig!\n')
else:
print(f'Feil! Riktig svar er: {riktig_svar}\n')Nå gjenstår det bare å skrive en for-hver løkke for å gå gjennom alle spørsmålene. Vi skriver også en funksjon som heter main() for å samle denne funksjonaliteten:
def main():
"""Hovedfunksjonen for å spille quizen."""
for spørsmål in quiz.values():
print(spørsmål['spørsmålstekst'])
print('\nSvaralternativer:')
for alternativ in spørsmål['alternativer']:
print(alternativ)
spiller_svar = input('Svar: ')
spørsmål['spiller_svar'] = spiller_svar
riktig_svar = spørsmål['riktig_svar']
if spiller_svar == riktig_svar:
print('Helt riktig!\n')
else:
print(f'Feil! Riktig svar er: {riktig_svar}\n')
if __name__ == '__main__':
main()9.7.4 Svaralternativer og poengsum
La oss nå legge til ekstrafunksjonaliteten som forbedrer applikasjonen. Først skal vi sjekke at spilleren faktisk velger en av svaralternativene. Her skriver vi en separert funksjon som trenger tilgang til både svaret som spilleren gir, og hvilke svaralternativer som eksisterer:
def sjekk_svaralternativ(svar, svaralternativer):
"""Sjekker om spillerens svar er et gyldig alternativ"""
return svar in svaralternativerDenne kan vi nå bruke som en hjelpefunksjon i hovedfunksjonen main() slik:
def main():
"""Hovedfunksjonen for å spille quizen."""
for spørsmål in quiz.values():
print(spørsmål['spørsmålstekst'])
print('\nSvaralternativer:')
for alternativ in spørsmål['alternativer']:
print(alternativ)
spiller_svar = input('Svar: ')
spørsmål['spiller_svar'] = spiller_svar
if not sjekk_svaralternativ(spiller_svar, spørsmål['alternativer']):
print('Dette er ikke et gyldig svaralternativ. Automatisk feil!')
continue
riktig_svar = spørsmål['riktig_svar']
if spiller_svar == riktig_svar:
print('Helt riktig!\n')
else:
print(f'Feil! Riktig svar er: {riktig_svar}\n')Dersom spilleren prøver å besvare noe som ikke er et svaralternativ, så blir det automatisk feil og vi går videre til neste spørsmål. Litt brutalt, men slik er det iblant.
Nå la oss beregne poengsum. Etter at spilleren er ferdig å spille, så har vi all informasjon vi trenger i quiz til å regne dette ut. Følgende funksjon gjør dette:
def beregn_poengsum():
"""Beregn poengsummen basert på spillerens svar"""
poengsum = 0
for spørsmål in quiz.values():
if spørsmål['spiller_svar'] == spørsmål['riktig_svar']:
poengsum += spørsmål['poeng']
return poengsumHer går vi gjennom hvert spørsmål i quizen og sjekker om spilleren har svart riktig. Hvis ja, så blir poengene lagt til poengsummen til spilleren. Vi kan nå legge dette til på bunnen av programmet slik:
Her blir da altså poengsummen regnet ut etter at hele spillet er ferdig. Det hadde vært litt nyttig for spilleren å forstå hvor mange totale poeng det er i spillet. Uten dette er det vanskelig å forstå hvor bra poengsummen spilleren har fått egentlig er. Dette kan vi gjøre enkelt med en generatorbeskrivelse slik:
if __name__ == '__main__':
main()
poengsum = beregn_poengsum()
total_poeng = sum(spørsmål['poeng'] for spørsmål in quiz.values())
print(f'Du fikk {poengsum} poeng av {total_poeng} mulige poeng!')Da er applikasjonen vår ferdig. Den endelige koden ser slik ut:
quiz = {
'spørsmål 1': {
'spørsmålstekst': 'Hvem vant toppserien i fotball for kvinner i 2024?',
'alternativer': ('Brann', 'Vålerenga', 'Rosenborg', 'Røa'),
'spiller_svar': '',
'riktig_svar': 'Vålerenga',
'poeng': 10
},
'spørsmål 2': {
'spørsmålstekst': 'Hvilket land vant VM i fotball for menn i 2022?',
'alternativer': ('Argentina', 'Frankrike', 'Tyskland', 'Brasil'),
'spiller_svar': '',
'riktig_svar': 'Argentina',
'poeng': 10
},
'spørsmål 3': {
'spørsmålstekst': 'Hvilket lag vant Champions League for menn i 2023?',
'alternativer': ('Manchester City', 'Liverpool', 'Chelsea', 'Milan'),
'spiller_svar': '',
'riktig_svar': 'Manchester City',
'poeng': 15
}
}
def sjekk_svaralternativ(svar, svaralternativer):
"""Sjekker om spillerens svar er et gyldig alternativ"""
return svar in svaralternativer
def beregn_poengsum():
"""Beregn poengsummen basert på spillerens svar"""
poengsum = 0
for spørsmål in quiz.values():
if spørsmål['spiller_svar'] == spørsmål['riktig_svar']:
poengsum += spørsmål['poeng']
return poengsum
def main():
"""Hovedfunksjonen for å spille quizen."""
for spørsmål in quiz.values():
print(spørsmål['spørsmålstekst'])
print('\nSvaralternativer:')
for alternativ in spørsmål['alternativer']:
print(alternativ)
spiller_svar = input('Svar: ')
spørsmål['spiller_svar'] = spiller_svar
if not sjekk_svaralternativ(spiller_svar, spørsmål['alternativer']):
print('Dette er ikke et gyldig svaralternativ. Automatisk feil!')
continue
riktig_svar = spørsmål['riktig_svar']
if spiller_svar == riktig_svar:
print('Helt riktig!\n')
else:
print(f'Feil! Riktig svar er: {riktig_svar}\n')
if __name__ == '__main__':
main()
poengsum = beregn_poengsum()
total_poeng = sum(spørsmål['poeng'] for spørsmål in quiz.values())
print(f'Du fikk {poengsum} poeng av {total_poeng}!')Her har vi valgt å legge beregningen av poengsum utenfor main() funksjonen. Du kunne like gjerne lagt den inni funksjonen main(). Skulle du ha jobbet videre med dette programmet burde du nok delt opp funksjonen main() i mindre funksjoner.
9.7.5 Refleksjoner
Det er to ting jeg vil du skal merke deg med applikasjonen vi har bygget:
Det at vi bygget en datamodell på forhånd gjorde arbeidet vårt betydelig lettere. Vi har en hovedvariabel
quizsom holder en ordbok for hvert spørsmål. Tenk om du bare hadde lagret hver bit med informasjon i en separert variabel. Siden hvert spørsmål har 5 biter med informasjon knyttet til seg ville dette gitt deg 100 variabler for en quiz med 20 spørsmål. Når du skulle regne sammentotal_poengpå slutten måtte du da addere 20 separerte variabler. Dette fører fort til feil, og koden blir vanskelig å lese.Vi har all informasjonen i variabelen
quizdirekte inne i kodefilen. Dette går fint med 3 spørsmål, men hadde du hatt 20 spørsmål ville nærmere 90 % av innholdet i kodefilen vært informasjon før det blir lagt på noe logikk. Dette er ikke helt ideelt. En bedre måte hadde vært å lagre informasjonen i egne filer som er bra egnet for rimelig strukturert data. Her er JSON-filer eller CSV-filer muligheter. For å kunne ta i bruk dette må vi lære hvordan lese og skrive til forskjellige filer i Python. Dette er temaet i neste kapittel!
9.8 Oppgaver
Oppgave 1
Hva gjør følgende kode? Beskriv hvert steg!
vekt_katter = {
'Mr Whiskers': 6.2,
'Millie': 5.5,
'Luna': 4.3,
'Simba': 6.8,
'Nala': 4.9,
'Pusur': 7.1
}
kattenavn = [navn for navn in vekt_katter]
vekt_etter_middag = {navn: vekt + 0.5 for (navn, vekt) in vekt_katter.items()}
total_vekt = sum(vekt for vekt in vekt_katter.values())Oppgave 2
Akkurat som med lister kan du legge tupler inni andre tupler. Her er kode som representerer noen av de mest ikoniske tidlige kongene våre med perioden de var konge:
konger = (
('Harald Hårfagre', (865, 930)),
('Eirik Blodøks', (930, 935)),
('Olav den Hellige', (1015, 1028)),
('Håkon Håkonsson', (1217, 1263))
)De nøyaktige periodene Harald Hårfagre og Eirik Blodøks var konger er ikke fullstendig klare. Likevel er tallene jeg har oppgitt greie estimater. Gjør følgende:
Skriv ut navnet på alle kongene.
Skriv en funksjon som regner ut hvor lenge hver konge regjerte.
Prøv å endre kongeperioden til Håkon Håkonsson slik at den er fra
1231til1282. Hvilken feil får du da?
Oppgave 3
Du og en venn har laget hver deres liste over band dere vil se på den kjente festivalen Tons of Rock i Oslo:
mine_band = ['Muse', 'Dream Theater', 'Thundermother', 'Electric Callboy']
vennens_band = ['Electric Callboy', 'Fit for an Autopsy', 'Muse']Bruk mengdeoperasjoner til å finne:
Band som minst én av dere vil se. Dette er perfekt til å lage en felles spilleliste før festivalen.
Band som dere begge vil se. Dette er bandene dere kan se sammen.
Band som du vil se, men som vennen din ikke vil se. Dette er band du må se alene.
Oppgave 4
Du er gitt følgende ordbok om nærliggende hovedsteder og deres innbyggertall:
hovedsteder = {
'Norge': {'hovedstad': 'Oslo', 'innbyggere': 710_000},
'Sverige': {'hovedstad': 'Stockholm', 'innbyggere': 988_000},
'Danmark': {'hovedstad': 'København', 'innbyggere': 667_000},
'Tyskland': {'hovedstad': 'Berlin', 'innbyggere': 3_780_000}
}Skriv en funksjon som returnerer landet som har flest innbyggere i hovedstaden.
Skriv en funksjon som returnerer en ny ordbok med kun de landene der hovedstaden har mer enn 800 000 innbyggere.
Oppgave 5 (Utfordrende)
Når du forsøker å hente ut mye informasjon fra vanlige tupler kan det fort bli utfordrende å lese:
konge = ('Harald Hårfagre', (865, 930))
print(f'Kongen {konge[0]} hadde periode {konge[1][0]} - {konge[1][1]}')Her er du helt avhengig av å huske rekkefølgen på hvordan første_konge er organisert. Et alternativ her er å bruke navngitte tupler92 som finnes i standardbiblioteket collections. Med navngitte tupler kan hvert element refereres til ved navn fremfor med en indeks.
Omskriv koden over ved å bruke navngitte tupler, slik at den blir mer lesbar. Du kan lese gjennom eksempelet nedenfor for inspirasjon. Her blir navngitte tupler brukt for å jobbe med superhelter:
from collections import namedtuple
Superhelt = namedtuple('Superhelt', ['navn', 'kraft', 'nivå'])
ironman = Superhelt('Iron Man', 'teknologi', 89)
print(ironman.navn) # Iron Man
print(ironman.kraft) # teknologi
print(ironman.nivå) # 89Grunnen til at Superhelt er skrevet med stor forbokstav er at det er en klasse. Vi skal lære mer om klasser i kapittel 11.