Slovarji

Vzemimo, spet, podatke o kolesarjenju, tiste, s štirimi stolpci. Za zdaj smo ugotovili le, kolikšni sta skupna prevožena razdalja in višina, podatek o kolesu pa kar zanemarili. Lahko bi izračunali, recimo, koliko je prevozil s Canyonom.

vozenj_canyon = 0
razdalja_canyon = 0
visina_canyon = 0

for vrstica in open("kolesa.txt"):
    kolo, razdalja, visina = vrstica.split(",")
    if kolo == "Canyon":
        vozenj_canyon += 1
        razdalja_canyon += int(razdalja)
        visina_canyon += int(visina)
    
print("Voženj:", vozenj_canyon)
print("Razdalja:", razdalja_canyon)
print("Višina:", visina_canyon)
Voženj: 28
Razdalja: 2932
Višina: 25630

Zdaj pa isto še za Cube, Nakamura in Stevens. Če damo to v isti program, bomo imeli štirikar po tri spremenljivke.

Primeri pa se lahko še kaj hujšega: v družini nas je pet in imamo osem koles. (Štirje po enega in eden štiri.)

Kako lepo bi bilo, če bi lahko imeli eno samo spremenljivko vozenj, ki bi imela "predalčke" Canyon, Cube, Nakamura in Stevens. In, seveda, spremenljivki razdalja in visina, vsako s štirimi predalčki.

Očitno to obstaja, sicer si tega ne bi želel, ne? Takšnim spremenljivkam - točneje, taki vrsti spremenljivk rečemo slovar - po angleško dictionary, ime Pythonovega podatkovnega tipa pa je dict.

Za začetek pripravimo nekaj preprostejšega: slovar z višinami hribov. Takšen bi bil:

hribi = {"Triglav": 2864, "Storžič": 2132, "Grintovec": 2558}

Slovar hribi ima predalčke "Triglav", "Storžič" in "Grintovec", vrednosti, shranjene v teh predalčkih pa so 2864, 2132 in 2558. Kot vidimo, slovar pripravimo tako, da med zavite oklepaje zapišemo pare z imenom predalčka (ki mu učeno rečemo ključ, key) in pripadajočimi vrednostmi (value), ločimo pa jih z dvopičji.

Python nam seveda lahko izpiše celotno vsebino slovarja.

hribi
{'Triglav': 2864, 'Storžič': 2132, 'Grintavec': 2558}

Do posameznih vrednosti v slovarju pridemo tako, da "predalček" podamo v oglatih oklepajih.

hribi["Triglav"]
2864
hribi["Storžič"]
2132

Če kak ključ ne obstaja, Python to jasno pove.

hribi["Stol"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[6], line 1
----> 1 hribi["Stol"]

KeyError: 'Stol'

Predalčki se vedejo kot spremenljivke. Triglavu lahko priredimo drugo višino

hribi["Triglav"] = 2800
hribi
{'Triglav': 2800, 'Storžič': 2132, 'Grintavec': 2558}

Lahko mu tudi prištejemo (zdaj manjkajočih) 64 metrov.

hribi["Triglav"] += 64
hribi
{'Triglav': 2992, 'Storžič': 2132, 'Grintavec': 2558}

Skratka, kot katerakoli druga spremenljivka.

Oboroženi z novim znanjem izračunajmo kolesarsko statistiko.

vozenj = {"Canyon": 0, "Cube": 0, "Nakamura": 0, "Stevens": 0}
razdalje = {"Canyon": 0, "Cube": 0, "Nakamura": 0, "Stevens": 0}
visine = {"Canyon": 0, "Cube": 0, "Nakamura": 0, "Stevens": 0}

for vrstica in open("kolesa.txt"):
    kolo, razdalja, visina = vrstica.split(",")
    vozenj[kolo] += 1
    razdalje[kolo] += int(razdalja)
    visine[kolo] += int(visina)

Pa zdaj? No, to kar potrebujemo je v slovarjih.

vozenj
{'Canyon': 28, 'Cube': 43, 'Nakamura': 29, 'Stevens': 0}
razdalje
{'Canyon': 2932, 'Cube': 2939, 'Nakamura': 488, 'Stevens': 0}
visine
{'Canyon': 25630, 'Cube': 70947, 'Nakamura': 1499, 'Stevens': 0}

Kaj pa, če bi radi to lepo izpisali? Recimo podatke za vsako kolo?

Čim rečemo "za vsako" nas to spomni na ... zanko for. Zanko for smo doslej naganjali prek datotekm, ko smo morali ponoviti kos programa za vsako vrstico datoteke. Kaj se zgodi, če jo poskusimo spustiti čez slovar?

for bogvekajboto in vozenj:
    print(bogvekajboto)
Canyon
Cube
Nakamura
Stevens

Zanka for daje ključe slovarja. Prav, če imamo ključ, potem že znamo priti do vrednosti.

for kolo in vozenj:
    print(kolo, "-", vozenj[kolo], "vozenj")
Canyon - 28 vozenj
Cube - 43 vozenj
Nakamura - 29 vozenj
Stevens - 0 vozenj

Ker imajo vsi trije slovarji tako ali tako iste ključe, lahko mimogrede izpišemo še skupno prevoženo višino in razdaljo.

for kolo in vozenj:
    print(kolo, "-", vozenj[kolo], "vozenj,", razdalje[kolo], "km,", visine[kolo], "m.")
Canyon - 26 vozenj, 2766 km, 26392 m.
Cube - 43 vozenj, 3174 km, 66705 m.
Nakamura - 22 vozenj, 439 km, 1119 m.
Stevens - 9 vozenj, 607 km, 3395 m.

Nadvse imenitno. No, kar imenitno. Nad vse(m) imenitno pa še ni, ker bo postalo še imenitnejše. :)

Dodajanje v slovar

Program se začne z

vozenj = {"Canyon": 0, "Cube": 0, "Nakamura": 0, "Stevens": 0}
razdalje = {"Canyon": 0, "Cube": 0, "Nakamura": 0, "Stevens": 0}
visine = {"Canyon": 0, "Cube": 0, "Nakamura": 0, "Stevens": 0}

Tule je videti nekaj prostora za izboljšave. Kot prvo: moramo res tipkati vsa ta imena koles in ničle? Ne bi šlo brez?

Kot drugo: da to deluje, moramo vnaprej vedeti, katere znamke koles se bodo pojavile v datoteki. Kaj, če to ni vnaprej znano? Kako bi programirali v tem primeru?

Naučiti se bomo morali tri stvari:

  1. ustvariti prazen slovar,
  2. preveriti, ali slovar ima določen predalček in
  3. v primeru, da ga nima, dodati nov predalček.

Vse troje se bo izkazalo za čisto preprosto.

Kako sestaviti prazen slovar, zlahka uganemo. Pač ničesar ne zapišemo med oklepaje.

vozenj = {}
vozenj
{}

Kako preveriti, ali slovar vsebuje določen ključ? Besedo in smo doslej uporabljali v zanki for, ko smo pisali, recimo for kolo in vozenj. Lahko jo tudi v if, oziroma, brez if, v logičnem izrazu, kot operator. Operator a in c vrne True, če c vsebuje a.

Še vedno imamo pri roki

razdalje
{'Canyon': 2766, 'Cube': 3174, 'Nakamura': 439, 'Stevens': 607}

Vidimo, da slovar vsebuje "Canyon", ne pa "Focus". Operator in se s tem videnjem strinja.

"Canyon" in razdalje
True
"Focus" in razdalje
False

Odlično, znamo preveriti, ali predalček obstaja. Kljukica pod točko 2.

Zdaj pa še dodajanje predalčka.

razdalje["Focus"] = 0
razdalje
{'Canyon': 2766, 'Cube': 3174, 'Nakamura': 439, 'Stevens': 607, 'Focus': 0}
hribi["Šmarna gora"] = 667
hribi
{'Triglav': 2992, 'Storžič': 2132, 'Grintavec': 2558, 'Šmarna gora': 667}

Pa imamo boljši program za kolesarsko statistiko.

vozenj = {}
razdalje = {}
visine = {}

for vrstica in open("kolesa.txt"):
    kolo, razdalja, visina = vrstica.split(",")
    if not kolo in vozenj:
        vozenj[kolo] = razdalje[kolo] = visine[kolo] = 0
    vozenj[kolo] += 1
    razdalje[kolo] += int(razdalja)
    visine[kolo] += int(visina)
    
for kolo in vozenj:
    print(kolo, "-", vozenj[kolo], "vozenj,", razdalje[kolo], "km,", visine[kolo], "m.")
Nakamura - 22 vozenj, 439 km, 1119 m.
Cube - 43 vozenj, 3174 km, 66705 m.
Canyon - 26 vozenj, 2766 km, 26392 m.
Stevens - 9 vozenj, 607 km, 3395 m.

Ker vsi trije slovarji bodisi vsebujejo bodisi ne vsebujejo kolesa, preverjamo le, ali je kolo že v slovarju vozenj. Če ga ni tam, ga tudi v ostalih ni, torej ga dodamo v vse.

Ker se not kolo in vozenj bere čudno, obstaja poleg operatorja in tudi operator not in.

"Scott" not in vozenj 
True

Slovarji - v splošnem

Ključi gornjega slovarja ("imena predalčkov") so bili nizi, vrednosti pa števila, int. Slovarji niso omejeni zgolj na te tipe.

Vrednosti so lahko karkoli: ne le int in float, temveč tudi nizi, logične vrednosti in vsi raznorazni tipi, ki jih bomo še spoznali pri predmetu. Slovar lahko vsebuje celo druge slovarje (uh, celo samega sebe!) kot vrednost.

Ključi so bolj omejeni. Ključi morajo biti nespremenljivi. Za naše potrebe in naše dosedanje znanje: ključ je lahko karkoli razen slovarja. "Prepovedanim" tipom ključev se bosta kmalu pridružila še seznam in množica, drugih prepovedanih tipov ključev pa pri tem predmetu ne boste spoznali. Vsaj ne takšnih, ki bi vas jih zamikalo uporabiti kot ključ.

Pri tem predmetu bodo ključi skoraj vedno nizi, včasih int-i in skoraj nikoli nič drugega.

Isti slovar ima lahko različne tipe ključev. In različne tipe vrednosti.

{"Ana": 129415, 4: True, 3.14: {}}
{'Ana': 129415, 4: True, 3.14: {}}

Če se komu zdi to slaba ideja, bo to zato, ker je dejansko slaba. Navadno bodo vsi ključi nekega slovarja in vse vrednosti nekega slovarja enakega tipa. Sicer ... no, kaj pravzaprav shranjujemo v slovarju, če se v njem pojavlja tako raznovrstna šara?!

Slovarji imajo tudi metode

Vsaka stvar v Pythonu ima metode. Niz, kot smo rekli, ducate. Slovarji jih imajo malo manj, a vseeno jih ne bomo našteli vseh, saj si jih tako ali tako ne bi zapomnili. Bomo že sproti videli, kar bomo potrebovali. (Kdor je neučakan, pa lahko pogleda dokumentacijo).

Vrednosti

Vzemimo slovar visine. Kakšna je skupna višina, ki jo je prevozil kolesar?

visine
{'Nakamura': 1119, 'Cube': 66705, 'Canyon': 26392, 'Stevens': 3395}
1119 + 66705 + 26392 + 3395
97611

Ne, nisem mislil tako. Ne bomo seštevali ročno. Kar program naj sešteje.

skupna = 0
for kolo in visine:
    skupna += visine[kolo]
    
skupna
97611

Če razmislimo, nas ključi tu pravzaprav ne zanimajo. Zanimajo nas le vrednosti. Tule bi nam prišlo bolj prav, če bi šla zanka for prek vrednosti, ne prek ključev.

Ni problema. Slovarji imajo metodo values, ki vrne vrednosti.

for visina in visine.values():
    print(visina)
1119
66705
26392
3395

In to seveda znamo sešteti.

skupna = 0
for visina in visine.values():
    skupna += visina
    
skupna
97611

Prej ko slej boste odkrili, da obstaja funkcija sum, ki lahko sešteje, kar ji podamo kot argument. Če ji podamo visine.values() bo seštela vse vrednosti v slovarju.

sum(visine.values())
97611

Torej:

print("Število voženj:", sum(vozenj.values()))
print("Skupna razdalja:", sum(razdalje.values()))
print("Skupna višina:", sum(visine.values()))
Število voženj: 100
Skupna razdalja: 6986
Skupna višina: 97611

Privzete vrednosti

Slovarji imajo metodo setdefault(kljuc, vrednost), ki za podani ključ nastavi podano vrednost; če ključ že obstaja, pa ne stori ničesar. V vsakem primeru pa vrne vrednost, ki pripada temu ključu - bodisi obstoječo (in torej nespremenjeno) vrednost, bodisi pravkar nastavljeno novo vrednost.

vozenj
{'Nakamura': 22, 'Cube': 43, 'Canyon': 26, 'Stevens': 9}
vozenj.setdefault("Scott", 0)
0
vozenj
{'Nakamura': 22, 'Cube': 43, 'Canyon': 26, 'Stevens': 9, 'Scott': 0}
vozenj.setdefault("Nakamura", 0)
22
vozenj
{'Nakamura': 22, 'Cube': 43, 'Canyon': 26, 'Stevens': 9, 'Scott': 0}

To poenostavi naše računanje.

vozenj = {}
razdalje = {}
visine = {}

for vrstica in open("kolesa.txt"):
    kolo, razdalja, visina = vrstica.split(",")
    vozenj.setdefault(kolo, 0)
    razdalje.setdefault(kolo, 0)
    visine.setdefault(kolo, 0)
    vozenj[kolo] += 1
    razdalje[kolo] += int(razdalja)
    visine[kolo] += int(visina)
    
for kolo in vozenj:
    print(kolo, "-", vozenj[kolo], "vozenj,", razdalje[kolo], "km,", visine[kolo], "m.")
Nakamura - 22 vozenj, 439 km, 1119 m.
Cube - 43 vozenj, 3174 km, 66705 m.
Canyon - 26 vozenj, 2766 km, 26392 m.
Stevens - 9 vozenj, 607 km, 3395 m.

Meh, ali pa tudi ne. Stavek if in malo prirejanja,

    if kolo not in vozenj:
        vozenj[kolo] = razdalje[kolo] = visine[kolo] = 0

smo zamenjali s tremi vrsticami setdefault-ov. No, če bi imeli en sam slovar, pa je

    d.setdefault(x, 0)

gotovo krajše in preprostejše kot

    if x not in d:
        d[x] = 0

Get

Za metodo get vam moram povedati preprosto zato, ker je to ena od stvari, za katere je boljše, da o njej slišite od modrih, starejših ljudi, kot od vrstnikov.

Metoda get vrne vrednost, ki pripada podanemu ključu.

visine
{'Nakamura': 1119, 'Cube': 66705, 'Canyon': 26392, 'Stevens': 3395}
visine.get("Cube")
66705

Ne počnite tega. V Pythonu se reče

visine["Cube"]
66705

V vaših programih bom sicer žalostno opazoval takšno uporabo get-a. Do nje bo mnogokrat prišlo, ker vam bodo pri reševanju nalog "pomagali" znanci, ki sicer programirajo v Javi ali podobno okornih jezikih, ki zaradi določenih omejitev ne morajo do vsebine slovarjev z oglatimi oklepaji.

Zakaj Python potem sploh ima get, če naj bi ga ne uporabljali?

Metodi get lahko podamo privzeto vrednost, ki naj jo vrne, če ključ ne obstaja.

visine.get("Cube", 0)
66705
visine.get("Focus", 0)
0

Zato in samo zato.

items

Videli smo, da gre zanka for, ko jo spustimo čez slovar, prek njegovih ključev. Če jo naženemo prek tega, kar vrne values, bo šla prek vrednosti. Slovarji imajo še metodo items, ki vrača pare ključev in vrednosti.

Vrnimo se malenkost nazaj. Spomnimo se, kako smo uporabljali rezultat split: ker je vračal več stvari, smo ga prirejali več spremenljivkam. Recimo, da imamo niz z dvema besedama, "Ana Benjamin" in ga razdelimo s split. Dobimo dve besedi, torej ju moramo shraniti v dve spremenljivki.

ona, on = "Ana Benjamin".split()
ona
'Ana'
on
'Benjamin'

Če metoda items vrača pare, bo tudi zanka for potrebovala dve spremenljivki, da ga shrani.

for kolo, visina in visine.items():
    print(kolo, " - ", visina)
Nakamura  -  1119
Cube  -  66705
Canyon  -  26392
Stevens  -  3395

Zdaj pa kar brž napišimo program, ki bo povedal, katero kolo se je največ vzpenjalo.

naj_visina = 0

for kolo, visina in visine.items():
    if visina > naj_visina:
        naj_visina = visina
        naj_kolo = kolo
        
print("Največ se je vzpenjal", naj_kolo, "in sicer", naj_visina, "metrov.")
Največ se je vzpenjal Cube in sicer 66705 metrov.

keys

Tudi ta je iz poglavja "boljše, da vam za to povem jaz".

keys vrne vse ključe slovarja.

for kolo in visine.keys():
    print(kolo)
Nakamura
Cube
Canyon
Stevens

To je seveda čisto drugače kot

for kolo in visine:
    print(kolo)
Nakamura
Cube
Canyon
Stevens

Aja, ni. Isto je.

Glede metode keys bi vam rad povedal le to: pozabite, da obstaja.

Prav, zakaj potem Python sploh ima to metodo?! Pojma nimam. Za hec sem vprašal za mnenje chatGPT. :) Takole me je podučil. (Kogar ne zanima krajši komentar chatGPTja, lahko z branjem tu zaključi.)

In Python, dictionaries (dict) have a keys() method that returns a view object containing the keys of the dictionary. While it might seem redundant since you can iterate over the dictionary itself to access its keys, there are some important distinctions and use cases for using dict.keys():

  1. View Object: dict.keys() returns a view object that is a dynamic view on the keys of the dictionary. This means that any changes made to the dictionary are immediately reflected in the view, and vice versa. This is not the case when you directly iterate over the dictionary.

  2. Compatibility: The .keys() method is available in Python 2 and Python 3, which makes it more backward-compatible for code that needs to run on both versions. Direct iteration over the dictionary may be less compatible.

  3. Explicitness: Using .keys() can make your code more explicit and easier to understand. It clearly signals your intention to work with the keys of the dictionary.

Nadaljeval je s primerom kode, ki je bila nesmiselna. Zaključil jo je s komentarjem:

In this example, the view object key_view reflects changes made to the dictionary, while iterating directly over the dictionary does not provide the same dynamic behavior. Depending on your use case and specific needs, you may choose to use dict.keys() or direct iteration, but understanding the differences and advantages of using dict.keys() can help you make informed decisions in your code.

Vem, da ne razumete, vendar: napisano nima smisla. Če spremenimo slovarm je slovar spremenjen. Prav tako je spremenjen "key_view". Oboje je "dinamično". "does not provide the same dynamic behavior" je tipičen primer chatGPTjevega flanca. Zadnji stavek pa sploh.

Tiste tri točke so pa zanimive. Tretja je mogoče smiselna in je stvar okusa.

Druga točka je popolnoma mimo. Res je, da imata oba metodo keys, vendar ne vrača iste stvari. Ena od največjih razlik med Pythonom 2 in 3 je prav v tem, kaj vračajo keys in podobne metode.

Prva točka je pa zanimiva. S keys lahko ustvarimo nek objekt, ki vsebuje le ključe, ne pa tudi vrednosti. Zakaj bi to hoteli, sicer ne vem, vendar ... no, tu je napisal nekaj smiselnega.