Poglejmo še enkrat sezname. Seznam je nekakšna zbirka nekih poljubnih reči, pri čemer je vsaka reč v svojem "predalčku", predalčki pa so oštevilčeni. Prvi ima številko 0, drugi 1, tretji 2 in tako naprej. Za vsak predalček lahko pogledamo, kaj je v njem, spremenimo njegovo vsebino, predalčke lahko odvzemamo in dodajamo.
>>> s = [6, 9, 4, 1] >>> s[1] 9 >>> s.append(7) >>> s [6, 9, 4, 1, 7] >>> del s[3] >>> s [6, 9, 4, 7] >>> s[-2:] [4, 7]
Slovar (angl. dictionary, podatkovni tip pa se imenuje
dict
) je podoben seznamu, le da se na predalčke sklicujemo z
njihovimi ključi (angl. key, ki so lahko števila, lahko
pa tudi nizi, logične vrednosti, terke... Temu, kar je shranjeno v predalčku
bomo rekli vrednost (value).
Seznam in slovar se že od daleč (če niste kratkovidni) razlikujeta po oglatosti. Seznam smo opisali tako, da smo v oglatih oklepajih našteli njihove elemente. Slovar opišemo tako, da v zavitih oklepajih naštejemo pare ključ: vrednost.
Stalno omenjani Benjamin si lahko takole sestavi slovar s telefonskimi številkami svojih oboževalk:
stevilke = {"Ana": "041 310239", "Berta": "040 318319", "Cilka": "041 103194", "Dani": "040 193831", "Eva": "051 123123", "Fanči": "040 135367", "Helga": "+49 175 4728 475"}
Do vrednosti, shranjenih v slovarju, pride podobno kot do vrednosti v seznamu: z indeksiranjem, le da v oglate oklepaje ne napiše zaporedne številke, temveč ključ.
>>> stevilke["Ana"] '041 310239' >>> stevilke["Dani"] '040 193831'
Slovarji ne poznajo vrstnega reda. V slovarju ni prvega, drugega, tretjega elementa. Ne le zato, ker oznake niso številke. Ne, slovar si v resnici ne zapomni vrstnega reda, v katerem smo našteli elemente, niti ne vrstnega reda, v katerem jih dodajamo.
>>> stevilke {'Dani': '040 193831', 'Fanči': '040 135367', 'Helga': '+49 175 4728 475', 'Eva': '051 123123', 'Cilka': '041 103194', 'Berta': '040 318319', 'Ana': '041 310239'}
(Čemu?! Čemu je to potrebno? Čemu zmeša vrstni red?! Mar ne more zlagati teh reči lepo po vrsti? Izvedeli boste drugo leto. V resnici ni zmešan, temveč prav premeteno premetan. V tem, drugače preurejenem seznamu lahko išče veliko hitreje.)
Ker ni vrstnega reda, tudi ni rezin.
>>> stevilke["Ana":"Cilka"] Traceback (most recent call last): File "", line 1, in TypeError: unhashable type
Tole sporočilo o napaki je sicer zelo čudno, vendar povsem smiselno. Boste razumeli, ko boste veliki. ;)
O "mehaniki" slovarjev morate vedeti le tole: slovarji so hitri, saj elementov v resnici ne iščejo. Na nek za zdaj skrivnosten način "vedo", kje je vrednost, ki pripada danemu ključu. Ne da bi iskali, vedo, kam morajo pogledati. Tudi dodajanje novih elementov v slovar je zelo hitro, enako tudi brisanje. Cena, ki jo plačamo za to, je dvojna. Ključi so lahko le podatkovni tipi, ki so nespremenljivi. Nespremenljivi podatkovni tipi so, vemo, nizi, števila, logične vrednosti in terke. Zgoraj smo kot ključ uporabili niz. To je OK. Če bi poskušali kot ključ uporabiti seznam, ne bi šlo. (V resnici je stvar malenkost bolj zapletena, ključ je lahko vsak podatkovni tip, ki je "razpršljiv", vendar to ni za bruce). Omejitve veljajo le za ključe. Vrednost je lahko karkoli.
Drugi obrok cene, ki jo plačamo za učinkovitost slovarjev, je poraba pomnilnika: slovar porabi nekako dvakrat več pomnilnika kot seznam. Koliko, točno, več, je odvisno od velikost slovarja in tega, kaj shranjujemo. Pri tem predmetu se s pomnilnikom ne obremenjujte.
Aha, še tretji obrok cene, iz drobnega tiska: vsak ključ se lahko pojavi največ enkrat. Če poskušamo prirediti neko vrednost ključu, ki že obstaja, le povozimo staro vrednost. Ampak to je tako ali tako pričakovati. Tudi v seznamu nimamo nikoli dveh različnih elementov na istem mestu.
Lahko jim dodajamo nove elemente: preprosto priredimo jim nov element.
>>> stevilke["Iva"] = "040 222333" >>> stevilke {'Dani': '040 193831', 'Fanči': '040 135367', 'Iva': '040 222333', 'Helga': '+49 175 4728 475', 'Eva': '051 123123', 'Cilka': '041 103194', 'Berta': '040 318319', 'Ana': '041 310239'}
append
ne obstaja, saj nima smisla: ker ni vrstnega reda, ne
moremo dodajati na konec. Prav tako (ali pa še bolj) ne obstaja
insert
, saj prav tako (ali pa še bolj) nima smisla.
Vprašamo se lahko, ali v seznamu obstaja določen ključ.
>>> "Cilka" in stevilke True >>> "Jana" in stevilke False
Če se Cilka skrega z Benjaminom, jo lahko le-ta pobriše (mislim, pobriše njeno številko).
>>> del stevilke["Cilka"] >>> stevilke {'Dani': '040 193831', 'Fanči': '040 135367', 'Iva': '040 222333', 'Helga': '+49 175 4728 475', 'Eva': '051 123123', 'Berta': '040 318319', 'Ana': '041 310239'} >>> "Cilka" in stevilke False
Če gremo prek slovarjev z zanko for
, dobimo vrednosti
ključev.
>>> for ime in stevilke: ... print(ime) ... Dani Fanči Iva Helga Eva Berta AnaSeveda lahko ob vsakem imenu izpišemo tudi številko.
>>> for ime in stevilke: ... print(ime + ":", stevilke[ime]) ... Dani: 040 193831 Fanči: 040 135367 Iva: 040 222333 Helga: +49 175 4728 475 Eva: 051 123123 Cilka: 041 103194 Berta: 040 318319 Ana: 041 310239
Vendar to ni najbolj praktično. Pomagamo si lahko s tremi metodami slovarja,
ki vrnejo vse ključe, vse vrednosti in vse pare ključ-vrednost. Imenujejo
se (ne preveč presenetljivo) keys()
, values()
in
items()
. Delamo se lahko, da vračajo sezname ključev,
vrednosti oziroma parov ... čeprav v resnici vračajo le nekaj, prek česar
lahko gremo z zanko for
.
Najprej poglejmo items()
.
>>> for t in stevilke.items(): ... ime, stevilka = t ... print(ime + ":", stevilka)
Tole zgoraj je bilo seveda grdo. Lepo se reče takole:
>>> for ime, stevilka in stevilke.items(): ... print(ime + ":", stevilke)
Tako kot sem ob zanki for težil, da ne uporabljajte
for i in range(len(s))
temveč for e in s
in da
že v glavi zanke razpakirajte terko, kadar je to potrebno, bom težil tudi
tule: uporabljajte items
in vaši programi bodo preglednejši in
s tem pravilnejši.
Metoda values
vrne vse vrednosti, ki so v slovarju. V našem
primeru torej telefonske številke. Metodo values
človek včasih
potrebuje, prav pogosto pa ne.
V nasprotju s tem metodo keys
potrebujejo samo študenti. Ne vem
točno, zakaj sploh obstaja. No, vem, zato da študenti lahko pišejo
for ime in stevilke.keys()
namesto
for ime in stevilke
. Druge uporabne vrednosti pa ne vidim. :)
Skratka: ne uporabljajte metode keys
.
Slovar ima še dve metodi, ki smo ju videli tudi pri seznamu: metodo, ki sprazni slovar in drugo, ki naredi nov, enak slovar.
>>> stevilke2 = stevilke.copy() >>> stevilke2 {'Dani': '040 193831', 'Fanči': '040 135367', 'Iva': '040 222333', 'Helga': '+49 175 4728 475', 'Eva': '051 123123', 'Berta': '040 318319', 'Ana': '041 310239'} >>> stevilke.clear() >>> stevilke {} >>> stevilke2 {'Dani': '040 193831', 'Fanči': '040 135367', 'Iva': '040 222333', 'Helga': '+49 175 4728 475', 'Eva': '051 123123', 'Berta': '040 318319', 'Ana': '041 310239'}
Omenimo le še eno od slovarjevih metod, get
. Ta dela podobno
kot indeksiranje, stevilke.get("Ana")
naredi isto kot
stevilke["Ana"]
. Metodo get
uporabimo, kadar ni
nujno, da je ključ v seznamu in želimo v tem primeru dobiti neko privzeto
vrednost. Privzeto vrednost podamo kot drugi argument.
>>> stevilke.get("Ema", "ni številke") '040 584928' >>> stevilke.get("Greta", "ni številke") 'nimam stevilke'
Še enkrat: ne pišite stevilke.get("Ema")
. To je čudno.
Piše se stevilke["Ema"]
. Metodo get
uporabite le
takrat, kadar niste prepričani, da slovar vsebuje ta ključ.
Neka naloga je šla takole.
Veliko latinskih napisov, ki obeležujejo kak pomemben dogodek, je napisanih v obliki kronograma: če seštejemo vrednosti črk, ki predstavljajo tudi rimske številke (I=1, V=5, X=10, L=50, C=100, D=500, M=1000), dajo letnico dogodka.
Tako, recimo, napis na cerkvi sv. Jakoba v Opatiji, CVIVS IN HOC RENOVATA LOCO PIA FVLGET IMAGO SIS CVSTOS POPVLI SANCTE IACOBE TVI, da vsoto 1793, ko je bila cerkev prenovljena (o čemer govori napis).
Pri tem obravnavamo vsak znak posebej: v besedil EXCELSIS bi prebrali X + C + L + I = 10 + 100 + 50 + 1 = 161 in ne XC + L + I = 90 + 50 + 1 = 141.
Napiši program, ki izračuna letnico za podani niz.
Očitna rešitev je:
def kronogram(s): v = 0 for c in s: if c=="I": v += 1 elif c=="V": v += 5 elif c=="X": v += 10 elif c=="L": v += 50 elif c=="C": v += 100 elif c=="D": v += 500 elif c=="M": v += 1000 return v
Pri njej vsi, ki poznajo stavek switch
oz. case
postokajo, kako je možno, da Python tega stavka nima. (Pustimo ob strani,
ali je tole res toliko daljše od switch
, sploh če program nespodobno
nabijemo skupaj:
def kronogram(s): v = 0 for c in s: if c=="I": v += 1 elif c=="V": v += 5 elif c=="X": v += 10 elif c=="L": v += 50 elif c=="C": v += 100 elif c=="D": v += 500 elif c=="M": v += 1000 return v
). Ne, nima ga, ker ga skoraj nikoli ne potrebujemo. Tisto, kar delamo z njim, pogosto rešimo (tudi) s slovarji.
V slovar si bomo zapisali, katero številko pomeni katera črka.
stevke = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
Funkcija zdaj deluje tako, da gre prek niza in za vsako črko preveri, ali je v slovarju. Če je ni, ne pomeni številke; če je, prištejemo toliko, kolikor je vredna.
def kronogram(s): v = 0 for c in s: if c in stevke: v += stevke[c] return v
Še hitreje gre z get
; če črke ni, je njena privzeta vrednost 0.
def kronogram(s): v = 0 for c in s: v += stevke.get(c, 0) return v
Ob tem si ne moremo kaj, da ne bi poškilili za en mesec naprej, ko se bomo učili funkcijskega programiranja in znali biti še krajši in jedrnatejši, spet predvsem po zaslugi slovarjev.
def kronogram(s): return sum(stevke.get(c, 0) for c in s)
Slovarji so uporabne reči. V veliko primerih pa uporabimo neko različico slovarja, ki ima še eno dodatno sposobnost.
Denimo, da smo dobili v preskušanje igralno kocko, za katero nas zanima, ali goljufa. Stokrat smo jo vrgli in zabeležili rezultate.
meti = [4, 4, 4, 3, 2, 3, 5, 3, 3, 4, 6, 6, 6, 1, 3, 6, 6, 4, 1, 4, 6, 1, 4, 4, 4, 6, 4, 6, 5, 5, 6, 6, 2, 4, 4, 6, 3, 2, 6, 1, 3, 6, 3, 2, 6, 6, 4, 6, 4, 2, 4, 4, 1, 1, 6, 2, 6, 6, 4, 3, 4, 2, 6, 5, 6, 3, 2, 5, 1, 5, 3, 6, 4, 6, 2, 2, 4, 1, 4, 4, 3, 1, 4, 2, 1, 3, 1, 4, 6, 1, 1, 3, 4, 1, 4, 3, 2, 4, 6, 6]
Zanima nas, kolikokrat je padla katera številka. Nič lažjega. Najprej si pripravimo seznam s sedmimi ničlami.
>>> kolikokrat = [0] * 7 >>> kolikokrat [0, 0, 0, 0, 0, 0, 0]
Zdaj nam bo, recimo kolikokrat[3]
povedal, kolikokrat je padla
trojka. (Čemu sedem? Saj ima kocka samo šest ploskev. Boste videli. Finta.)
Zdaj pa štejmo: pojdimo čez vse mete in povečujmo števce.
>>> for met in meti: ... kolikokrat[met] += 1 ... >>> kolikokrat [0, 14, 12, 15, 27, 6, 26]
(Kje je finta? kolikokrat[0]
bo pove, kolikokrat je padla ničla.
Nikoli pač. Napišite isto reč s seznamom dolžine šest, ne sedem. Ni problem,
ampak tako, kot smo nardili je preprostejše.)
(Kocka je res videti nekoliko sumljiva: štirke in šestice so nekam pogoste na račun petk. A pustimo statistikom vprašanje, ali je to še lahko naključno ali ne.)
Ups, nalogo smo rešili kar s seznamom! Da, to gre, ker so "predalčki" seznama oštevilčeni, tako kot ploskve kocke. Tole pa ne bo šlo: tule je seznam telefonskih številk deklet, ki jih je danes klical Benjamin. Katero je poklical kolikokrat?
klici = ['041 103194', '040 193831', '040 318319', '040 193831', '041 310239', '040 318319', '040 318319', '040 318319', '040 193831', '040 193831', '040 193831', '040 193831', '040 193831', '040 318319', '040 318319', '040 318319', '040 193831', '040 318319', '041 103194', '041 103194', '041 310239', '040 193831', '041 103194', '041 310239', '041 310239', '040 193831', '041 310239', '041 103194', '040 193831', '040 318319']
Za tako velike številke bi moral imeti seznam zelo veliko predalčkov. Še huje bi bilo, če bi namesto seznama številk dobili seznam klicanih oseb.
klici = ['Cilka', 'Dani', 'Berta', 'Dani', 'Ana', 'Berta', 'Berta', 'Berta', 'Dani', 'Dani', 'Dani', 'Dani', 'Dani', 'Berta', 'Berta', 'Berta', 'Dani', 'Berta', 'Cilka', 'Cilka', 'Ana', 'Dani', 'Cilka', 'Ana', 'Ana', 'Dani', 'Ana', 'Cilka', 'Dani', 'Berta']
Tu smo s seznamom pogoreli. No, ne čisto; rešitev s seznami seveda obstaja, je pa zoprna - podobna je tistemu, kar smo v začetku počeli z bančnimi računi.
S slovarji je veliko lepše:
pogostosti = {} for ime in klici: if ime not in pogostosti: pogostosti[ime] = 0 pogostosti[ime] += 1 print(pogostosti)
Ob vsakem novem klicu preverimo, ali je klicano ime
že v
slovarju. Če ga ni, da dodamo. Nato - najsibo ime novo ali ne - povečamo
števec klicev pri tej osebi.
Ker je ta stvar resnično pogosta, nam Python pomaga z modulom
collections
, ki vsebuje podatkovni tip defaultdict
.
(Modul, ki vsebuje podatkovni tip?! Da, da; tudi to je ena od stvari, ki jih
bomo našli v modulih.) Ta se obnaša tako kot slovar, z eno izjemo: če zahtevamo
kak element, ki ne obstaja, si ga meni nič tebi nič izmisli. Točneje, doda ga v
slovar in mu postavi privzeto vrednost. Katero, določimo. Pri tem ne podamo
privzete vrednosti, temveč "funkcijo", ki bo vračala privzeto vrednost.
defaultdict
bo ustvarjal te, nove vrednosti tako, da bo poklical
to funkcijo brez argumentov in kot privzeto vrednost uporabil, kar vrne
funkcija, ki jo v ta namen vsakič sproti pokliče.
Zelo pogosto bo privzeta vrednost 0 in funkcija, ki vrača
0, se imenuje, hm, int
.
("Funkcija" int
, je vedno sumljivejša in sumljivejša. Že od
začetka smo v zvezi z njo dajali besedo "funkcija" pod narekovaje, zdaj pa vidimo,
da zmore vedno več in več stvari. Pa še enako ji je ime kot tipu in funkcij, ki
jim je ime enako kot tipom, je vedno več. Kakšna skrivnost se skriva za tem? To
boste izvedeli v enem od prihodnjih napetih nadaljevanj Programiranja 1.)
Preštejmo torej še enkrat Benjaminove klice, tokrat s slovarjem s privzetimi vrednostmi.
import collections pogostosti = collections.defaultdict(int) for ime in klici: pogostosti[ime] += 1
Ni to kul?
Poglejmo si nekaj, kar je kul še bolj.
Preštevanje je pravzaprav tako pogosta reč, da obstaja zanj specializiran
tip. Tako kot defaultdict
je v modulu collections
,
imenuje pa se Counter
.
>>> stevilo_klicev = collections.Counter(klici) >>> stevilo_klicev Counter({'Dani': 11, 'Berta': 9, 'Cilka': 5, 'Ana': 5})
Komentirali ne bomo veliko, ker še ne znamo. Že ob tem, da sem temu rekel tip, sem se moral ugrizniti v jezik, saj bi raje govoril o razredu. Kaj je in kako deluje, pa nas še presega. Zna pa pravzaprav še veliko stvari, tako da tem, ki jih zanima, priporočam, da si ga ogledajo. Mimogrede:
>>> napis = "CVIVS IN HOC RENOVATA LOCO PIA FVLGET IMAGO SIS" \ "CVSTOS POPVLI SANCTE IACOBE TVI" >>> collections.Counter() Counter({' ': 13, 'I': 8, 'O': 8, 'V': 7, 'A': 6, 'C': 6, 'S': 6, 'T': 5, 'E': 4, 'L': 3, 'N': 3, 'P': 3, 'G': 2, 'B': 1, 'F': 1, 'H': 1, 'M': 1, 'R': 1})
Se pravi, da lahko kronogram rešimo tudi z
def kronogram(s): crke = collections.Counter(s) return crke["I"] + 5 * crke["V"] + 10 * crke["X"] + 50 * crke["L"] + \ 100 * crke["C"] + 500 * crke["D"] + 1000 * crke["M"]
Množice so podobne seznamom, a s to razliko, da lahko vsebujejo vsak element samo enkrat. Po drugi strani (in ne le po drugi strani, tudi tehnično) pa so podobne slovarjem. Niso namreč urejene in vsebujejo lahko le elemente, ki so nespremenljivi. Poleg tega pa lahko zelo hitro ugotovimo, ali množica vsebuje določen element ali ne - tako kot lahko pri slovarjih hitro ugotovimo, ali vsebujejo določen ključ ali ne.
Množice zapišemo z zavitimi oklepaji, tako kot smo vajeni pri matematiki.
danasnji_klici = {"Ana", "Cilka", "Eva"}
Tako lahko sestavimo le neprazno množico. Če napišemo le oklepaj in
zaklepaj, {}
, pa dobimo slovar. (Čemu so se odločili, naj bo
to slovar, ne množica? Slovar je bil prej, množice je Python dobil kasneje.
Zato.) Če hočemo narediti prazno množico, rečemo
prazna = set()
"Funkcija" set
je malo podobna "funkciji" int
:
damo ji lahko različne argumente, pa jih bo spremenila v množico. Damo ji
lahko, recimo, seznam, pa bomo dobili množico z vsemi elementi, ki se
pojavijo v njem.
>>> set([1, 2, 3]) {1, 2, 3} >>> set(range(5)) {0, 1, 2, 3, 4} >>> set([6, 42, 1, 3, 1, 1, 6]) {1, 42, 3, 6}
Mimogrede opazimo, da tudi množice, tako kot slovarji, res ne dajo nič na vrstni red.
Poleg seznamov lahko množicam podtaknemo karkoli, prek česar bi lahko šli
z zanko for
, recimo niz ali slovar.
>>> set("Benjamin") {'a', 'B', 'e', 'i', 'j', 'm', 'n'} >>> stevilke {'Dani': '040 193831', 'Fanči': '040 135367', 'Iva': '040 222333', 'Helga': '+49 175 4728 475', 'Eva': '051 123123', 'Cilka': '041 103194', 'Berta': '040 318319', 'Ana': '041 310239'} >>> set(stevilke) {'Iva', 'Helga', 'Eva', 'Berta', 'Fanči', 'Dani', 'Cilka', 'Ana'}
Spremenljivka stevilke
(še vedno) vsebuje slovar, katerega
ključi so imena Benjaminovih oboževalk. Ker zanka prek slovarja "vrača"
ključe, bo tudi množica, ki jo sestavimo iz slovarja, vsebovala ključe.
V množico lahko dodajamo elemente in vprašamo se lahko, ali množica vsebuje določen element.
>>> s = set("Benjamin") >>> "e" in s True >>> "u" in s False >>> s.add("u") >>> s {'a', 'B', 'e', 'i', 'j', 'm', 'n', 'u'} >>> "u" in s True >>> s.add("a") >>> s.add("a") >>> s.add("a") >>> s {'a', 'B', 'e', 'i', 'j', 'm', 'n', 'u'}
Na koncu smo poskušali v množico dodati element, ki ga že vsebuje. To seveda ne gre, množica vsak element vsebuje le enkrat.
Če imamo dve množici, lahko izračunamo njuno unijo, presek, razliko...
>>> u = {1, 2, 3} >>> v = {3, 4, 5} >>> u | v {1, 2, 3, 4, 5} >>> u & v {3}
Preverimo lahko tudi, ali je neka množica podmnožica (ali nadmnožica druge). To najpreprosteje storimo kar z operatorji za primerjanje.
>>> u = {1, 2, 3} >>> {1, 2} <= u True >>> {1, 2, 3, 4} <= u False >>> {1, 2, 3} <= u True >>> {1, 2, 3} < u False
{1, 2, 3}
, je podmnožica u
-ja, ni pa njegove
prava podmnožica, saj vsebuje kar cel u
.
Z množicami je mogoče početi še marsikaj zanimivega - vendar bodi dovolj.
Recimo, da želimo sestaviti skupine datotek v nekem direktoriju glede na končnice. Sestavili bomo slovar, katerega ključi bodo končnice, na primer .py, .txt in .mp3, vrednosti pri vsakem ključu pa imena datotek s to končnico.
datoteke = {} for ime in os.listdir(dir): konc = os.path.splitext(ime)[1] if not konc in datoteke: datoteke[konc] = set() datoteke[konc].add(ime)
Če ste bili pozorni, niste spregledali, da je gornji program pravzaprav na nek način enak programu, s katerim smo preštevali, kolikokrat je Benjamin klical katero od deklet:
pogostosti = {} for ime in klici: if not ime in pogostosti: pogostosti[ime] = 0 pogostosti[ime] += 1
"Istost" je v tem, da obakrat naredimo prazen slovar. Nato gremo z zanko
for
čez neke reči. Za vsako reč (v gornjem primeru datoteke, v
spodnjem ime dekleta) preverimo, ali je že v slovarju. Če je ni, jo dodamo tako,
da ji damo neko privzeto vrednost (zgoraj prazno množico, spodaj 0). Nato
pravkar dodano stvar posodobimo glede na trenutno reč (v gornjem primeru dodamo
datoteko v pravkar sestavljeno ali od prej obstoječo množico, v spodnjem povečamo
število klicev te osebe za 1).
Aja, to finto pa že poznamo. Slovarji s privzetimi vrednostmi!
datoteke = collections.defaultdict(set) for ime in os.listdir(dir): konc = os.path.splitext(ime)[1] datoteke[konc].add(ime)