Slovar

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.

Kaj lahko počnemo s slovarji?

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
Ana

Seveda 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č.

Primer: kronogrami

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 s privzetimi vrednostmi

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.

Števec

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

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.

Primer: Seznami v slovarjih

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)