# Moduli

Povedali smo že: Python ima tisoče in tisoče funkcij, zato morajo biti urejene, postavljene tja, kjer jih bomo lahko našli.

Enega od principov smo že spoznali: metode. Metode so pripete na reči (bolj učeno in pravilno: objekte) in počnejo stvari, tipične za te reči. Nizi imajo metode, kot so `lower` in `split`, slovarji pa `get` in `setdefault`.

Poleg metod ima Python tudi funkcije, ki ne spadajo k nobenemu specifičnemu tipu. Primera sta `input` in `print`. A teh je malo; privilegij ležati kar tako, na prostem, ima le nekaj zelo splošno uporabnih funkcij.

Večina funkcij, ki niso metode, je pospravljenih v škatlice. Škatlicam se v Pythonu reče modul (angl. module). Najbrž boste slišali tudi izraz paket (package) in celo knjižnica (library). Zaradi njih se ne vznemirjajte: paket je hierarhična zbirka modulov, knjižnica pa neka zaokrožena celota različnih ... modulov, paketov ... česarkoli. Za nas so tule pomembni samo moduli. Izvedeli bomo, kako jih uporabljati in videli nekaj uporabnih reči v nekaj uporabnih modulih.

## Uvažanje modulov

Da uporabimo funkcije v modulu, moramo modul najprej uvoziti.

Doslej smo uporabljali, na primer, funkcije iz modula `math` in uvažali smo jih s `from math import *`. Pri poučevanju programiranja se izogibam temu, da bi morali tipkati kakšne fraze, ki jih ne razumete. `from math import *` je bila ena redkih in čas je, da izvemo, za kaj gre. Ter tudi čas, da se te fraze odvadimo in se naučimo pisati kot se spodobi.

Modul z matematičnimim funkcijami, `math`, pravilno uvozimo tako:

    import math

S tem v bistvu dobimo novo spremenljivko, `math`.

    math

    <module 'math' from '/Users/janez/opt/miniconda3/envs/prog/lib/python3.11/lib-dynload/math.cpython-311-darwin.so'>

Tole je spremenljivka najbolj čudnega tipa doslej. Ni preprosto število (`int`, `float`) ali niz (`str`) ali seznam ali terka (`list`, `tuple`), temveč spremenljivka vrste "modul" ("module").

Funkcije pripadajo modulu, tako kot metode objektu. če imamo niz `ime`, bomo do njegove metode `upper` prišli s piko, `ime.upper`. Tu pa je podobno: če imamo modul `math`, bomo do njegove funkcije `sqrt` prišli z `math.sqrt`, do njegove spremenljivke `pi` pa z `math.pi`.

    math.pi

    3.141592653589793

    math.sin(math.pi / 4)

    0.7071067811865475

Uvozimo še en modul, `os`.

    import os

V `os` so funkcije, s katerimi lahko ustvarimo direktorij ali pobrišemo datoteko. Poleg tega pa vsebuje še nekaj zanimivega: modul. Modul `path` je modul znotraj modula `os`. Vsebuje, recimo, funkcijo `splitext`, ki za podano ime datoteke vrne osnovo in končnico.

    osnova, koncnica = os.path.splitext("nek_film.avi")

    osnova

    'nek_film'

    koncnica

    '.avi'

Tako se uvaža module.

### Uvažanje posamičnih funkcij

Če funkcije določenega modula kličemo velikokrat in predvsem, če to počnemo v aritmetičnih izrazih, to postane nepregledno.

    x = 2 * math.sin(math.radians(phi) + math.pi / 2) - 2 * math.cos(math.radians(alpha))

Boljše bi bilo imeti te funkcije pri roki brez ponavljajočega se `math`. Uvozimo jih z

    from math import sin, cos, radians, pi

    cos(pi / 4)

    0.7071067811865476

`from math import sin, cos, radians, pi` uvozi naštete funkcije in postanejo, pod temi imeni, dostopne programu. Imeli bomo torej imena `sin`, `cos`, `radians` in `pi`, ne pa tudi `tan` in `log`. Predvsem pa ne `math`.

Se pravi:

- če uvozimo `import math`, imamo `math` in, recimo `math.cos`, ne pa `cos`. Uvozili smo pač `math`, ne `cos`.
- če uvozimo `from math import cos` pa imamo `cos` in ne `math` ali `math.cos`. Uvozili smo pač `cos` in ne `math`.

### Uvažanje vsega

Obstaja še "preprostejši" način uvažanja.

    from math import *

Ta je podoben prejšnjemu, le da funkcij ne naštevamo. `*` predstavlja vse, kar je v modulu.

Prvi način, uvažanje modula, ima to prednost, da je v vsakem klicu funkcije jasno, odkod ta funkcija prihaja. Slabost so precej daljši izrazi.

Drugi način, uvažanje posamičnih funkcij skrajša izraze. Odkod prihaja kakšna funkcija, še vedno vidimo, vendar je potrebno za to pogledati na začetek programa, v `import`-e.

Tretji način je v splošnem slab. Izrazi so krajši, vendar za posamične funkcije ne vemo, odkod so prišle. Na ta način uvažamo kvečjemu modul `math` in nobenega drugega. Posebej v večjih projektih.

Sprememba: gornji odstavek sem napisal pred desetimi leti. V resnici že dolgo, dolgo nisem uvozil modula na takšnen način, razen v prvih tednih Programiranja 1. Tudi vam ga ni treba. To se ne dela.

## Kje uvažamo

Vedno na začetku programa. Ne kar tako, vmes.

Izjemoma: na začetku funkcije. To storimo, recimo, kadar ni jasno, ali bo modul možno uvoziti, oziroma je uvažanje počasno, potrebujemo pa ga le v neki funkciji. Druga situacija, kjer bi to prišlo prav, je, če se dva modula uvažata vzajemno, eden drugega. To razrešimo tako, da določen modul uvozimo šele, ko ga potrebujemo.

To ni zakon, samo dogovor.

Uvažanje modulov se v resnici zgodi samo enkrat. Če petkrat napišemo `import math`, se bo modul v resnici uvozil le prvič.

## Modul `math`

Ogledali si bomo nekaj uporabnih modulov. Python jih ima približno dvesto; še tisoče in tisoče jih najdete na internetu.

Prvi je `math`, vendar o njem ne bomo povedali veliko. Ima pač vse funkcije, ki si jih ima katerikoli kalkulator ... in še malo več. :) Na primer največji skupni delitelj, fakulteto in binomske koeficiente ...

Priložnost izkoristimo le za to, da omenimo dve posebnosti v zvezi s števili. Podatkovni tip `float` premore poleg normalnih števil še dve posebni: neskončno (in minus neskončno) ter ni-število (not a number).

    math.inf

    inf

    -math.inf

    -inf

    math.nan

    nan

Prvi dve sta lahko uporabni za kako iskanje minimuma ali maksimuma.

    def min(s):
        m = math.inf
        for x in s:
            if x < m:
                m = x
        return m

`math.inf` je večji od vseh števil (razen od sebe) in torej uporabna začetna vrednost pri iskanju minimuma.

`nan` je zanimivejša reč. V nekaterih jezikih ga dobimo kot rezultat napačnih argumentov za matematične funkcije, recimo, če poskušamo izračunati koren ali logaritem negativnega števila. Pythonove funkcije v tem primeru vrnejo napako, pač pa ga vrnejo nekatere funkcije v knjižnicah (modulih), ki si jih bomo namestili dodatno, zato je prav, da veste zanj.

`nan`, not a number, je slepa ulica števil. Karkoli počnemo z njim - ga seštevamo, odštevamo, množimo, celo množimo z nič - vedno ostane `nan`. Še huje: `nan` ni ne večji ne manjši od nobenega števila. Niti od neskončno ni manjši (in seveda ne večji, pa tudi enak ne).

    math.nan < math.inf

    False

    math.nan > math.inf

    False

    math.nan == math.inf

    False

In, najhujše, enak ni niti sebi.

    math.nan == math.nan

    False

Kako potem preverimo, če ima neka spremenljivka vrednost `nan` - če je ne moremo primerjati niti z `nan`?

    x = math.nan

    if x == math.nan:
        print("Ojoj!")
    else:
        print("Vse je OK.")

    Vse je OK.

Pač, izkoristimo njegovo neumnost proti njemu samemu.

    x = math.nan

    if x != x:
        print("Svet se podira!")

    Svet se podira!

Lepši, pravilnejši način, da preverimo, ali ima neka spremenljivka vrednost `nan`, je funkcija `math.isnan`.

    if math.isnan(x):
        print("Ojoj!")

    Ojoj!

Na vse to se bo koristno spomniti, ko bomo delali z, na primer, kakšnimi statističnimi knjižnicami, ki bodo takrat, ko česa ne bo možno izračunati, vrnili `nan`.

Mimogrede, tole čudno obnašanje, po katerem `nan` ni enak niti samemu sebi, ni kakšna Pythonova muha, temveč del standarda IEEE 754, ki določa zapis števil s plavajočo vejico (float) in njegovo vedenje. Enako se vede vsak vzgojen jezik.

## Modul `random`

Modul `random` vsebujejo funkcije, ki počnejo (psevdo-)naključne stvari. (To je: videti so naključne, vendar niso, saj je vse, kar naračunajo običajni računlniki, izračunano in ne izžrebano.) Omenili jih bomo le nekaj.

`random.random()` vrne naključno število med 0 in 1.

    import random

    random.random()

    0.8088080148072502

    random.random()

    0.5502323145363647

`random.uniform(a, b)` vrne naključno število med `a` in `b`. Funkcija se imenuje `uniform`, ker gre za enakomerno porazdelitev - vsa števila so enako verjetna.

    random.uniform(10, 20)

    11.801216989018465

    random.uniform(10, 20)

    10.786154792833283

Če potrebujemo celo naključno število, pokličemo `randint`.

    random.randint(10, 20)

    13

Če nas namesto enakomerne zanima kakšna druga porazdelitev: modul `random` jih pozna veliko: beta, gama, eksponentna ... in seveda tudi Gaussova.

    iq = random.gauss(100, 10)

    iq

    87.46911052444023

Sestavimo seznam imen.

    imena = ["Ana", "Berta", "Cilka", "Dani", "Ema", "Fanči"]

`random.choice` vrne naključni element podanega seznama.

    random.choice(imena)

    'Ana'

`random.sample` izbere naključni vzorec podane velikosti.

    random.sample(imena, 3)

    ['Cilka', 'Fanči', 'Berta']

`random.choices` je podoben, vendar se izbrani primeri lahko tudi ponavljajo. Velikost vzorca mora biti podana kot argument z imenom `k`.

    random.choices(imena, k=5)

    ['Dani', 'Cilka', 'Cilka', 'Cilka', 'Cilka']

`random.shuffle` premeša podani seznam.

    imena

    ['Ana', 'Berta', 'Cilka', 'Dani', 'Ema', 'Fanči']

    random.shuffle(imena)

    imena

    ['Cilka', 'Berta', 'Ana', 'Fanči', 'Dani', 'Ema']

## Modul `os`

Modul `os` vsebuje kop reči, povezanih z operacijskim sistemom. Ker o operacijskih sistemih ne vemo dovolj, večine funkcij ne bi razumeli. (Ta prva oseba množine velja tudi zame. Sicer večino razumem, o mnogih pa bi moral prebrati malo več, da bi vedel, za kaj pravzaprav gre.) Prgišče pa jih bomo vseeno redno uporabljali.

- `getcwd()` vrne trenutni direktorij. Torej direktorij, v katerem bi funkcija `open` iskala datoteko, če bi ji podali le ime datoteke.
- `chdir(path)` spremeni trenutni direktorij. Pot je lahko absolutna (s `/` na začetku) ali relativna.
- `mkdir(path)` naredi nov direktorij.
- `remove(filename)` pobriše datoteko s podanim imenom. Brez milosti. Nobenega "ali res želite pobrisati".
- `rename(name, newname)` preimenuje datoteko.
- `listdir(path)` vrne seznam vseh imen datotek v podanem direktoriju.

Največkrat boste potrebovali slednjo. Podatke iz vremenskih postaj, recimo, sem dobil v obliki tisočev datotek, ki sem jih potem prebral in (seveda filtrirane) podatke zapisal v novo datoteko.

Tole je trenutna vsebina trenutnega direktorija (v katerem so tile zapiski).

    import os

    os.listdir()

    ['temperature.svg',
     '08b Numpy - naslednji koraki.ipynb',
     'vremenske-postaje.txt',
     'xml - zapiski.md',
     'Untitled1.ipynb',
     'kolesa.txt',
     'avtorji-na-b.md',
     '.DS_Store',
     'kolesa2.txt',
     '03c Slovarji.ipynb',
     '06 Mnozice.ipynb',
     'temperature.txt',
     'Untitled.ipynb',
     '07 Pisanje datotek (in se malo branja).ipynb',
     'tecajnica.json',
     '06 Moduli.ipynb',
     '04 seznami.ipynb',
     'vreme',
     'authors-b.html',
     '03a Kako racunalnik shrani besedilo.ipynb',
     'temperature.pdf',
     'test.json',
     '09 Analiza podatkov in risanje.ipynb',
     '12 Python in Excel.ipynb',
     'temperature.png',
     '11 Markdown in Jupyter, HTML, regularni izrazi.ipynb',
     'xml',
     '08 Osnove numpyja.ipynb',
     'test.pkl',
     '02b logični izrazi.ipynb',
     'tecajnica.pickle',
     '.ipynb_checkpoints',
     '02 datoteke, zanke, pogoji.ipynb',
     'besedilo.txt',
     '05 Zanka `while`.ipynb',
     '04 Terke.ipynb',
     '10 Branje podatkov v standardnih formatih.ipynb',
     'x.html',
     'december.txt',
     '05 Funkcije.ipynb',
     '03b Metode nizov.ipynb',
     'kolesa-z-glavo.txt',
     '07 Oblikovanje nizov.ipynb']

## Modul `os.path`

`path` je modul znotraj modula `os`. Tudi ta ima marsikaj, nekaj funkcij pa je primernih tudi za nas.

- `os.path.exists(name)` vrne `True`, če (v trenutnem direktoriju) obstaja datoteka ali direktorij s podanim imenom.
- `os.path.isdir(name)` vrne `True`, če je podano ime direktorij (znotraj trenutnega direktorija).
- `os.path.isfile(name)` vrne `True`, če je podano ime datoteka (znotraj trenutnega direktorija).
- `os.path.splitext(name)` vrne osnovo in končnico podanega imena datoteke.

Ostale so očitne, za nas je posebej zanimiva zadnja.

Izpišimo imena vseh datotek s končnico .txt, ki se nahajajo v trenutnem direktoriju.

    for ime in os.listdir():
        osnova, koncnica = os.path.splitext(ime)
        if os.path.isfile(ime) and koncnica == ".txt":
            print(ime)

    vremenske-postaje.txt
    kolesa.txt
    kolesa2.txt
    temperature.txt
    besedilo.txt
    december.txt
    kolesa-z-glavo.txt

## Modul `collections`

Med vsemi zanimivimi rečmi v `collections` vsaj za zdaj omenimo le dve.

### `defaultdict`

`defaultdict` je slovar, ki sproti dodaja neobstoječe ključe, podati pa mu moramo funkcijo, s katero si bo izmišljal njihove vrednosti. Primerne so le funkcije, ki ne sprejemajo argumentov, oziroma, točneje, funkcije, ki jih lahko pokličemo (tudi) brez argumentov. Najpogosteje bo to `int`. Če jo pokličemo brez argumentov namreč vrne `0`.

    int()

    0

    from collections import defaultdict

    d = defaultdict(int)

    d["Ana"] = 5
    d["Berta"] = 3

    d

    defaultdict(int, {'Ana': 5, 'Berta': 3})

Doslej nič posebnega, le pri izpisu nam ne pokaže samo slovarja, temveč doda še, da je to `defaultdict` s funkcijo `int`. Zabava se začne, ko povprašamo po ključu, ki ne obstaja. Tu pokliče podano funkcijo (`int`) in vrne njeno vrednost, novi par pa doda tudi v slovar.

    d["Cilka"]

    0

    d

    defaultdict(int, {'Ana': 5, 'Berta': 3, 'Cilka': 0})

Smemo celo, recimo, povečati vrednost neobstoječemu ključu.

    d["Dani"] += 1

    d

    defaultdict(int, {'Ana': 5, 'Berta': 3, 'Cilka': 0, 'Dani': 1})

Preštejmo, kolikokrat se je avtor datoteke kolesa.txt vozil s katerim kolesom in koliko je prevozil z njim. Spomnimo se: vrstice datoteke predstavljajo posamične poti in vsebujejo ime kolesa, razdaljo v kilometrih in še nekaj, kar nas tu ne zanima (višino).

    uporaba = defaultdict(int)
    pot = defaultdict(int)

    for vrstica in open("kolesa.txt"):
        kolo, razdalja, _ = vrstica.split(",")
        uporaba[kolo] += 1
        pot[kolo] += int(razdalja)

    print(uporaba)
    print(pot)

    defaultdict(<class 'int'>, {'Nakamura': 22, 'Cube': 43, 'Canyon': 26, 'Stevens': 9})
    defaultdict(<class 'int'>, {'Nakamura': 439, 'Cube': 3174, 'Canyon': 2766, 'Stevens': 607})

Enako bi lahko naredili tudi z običajnimi slovarji, vendar bi zahtevalo nekaj `if`-ov ali pa metod, kot sta `setdefault` ali `get`.

Zdaj pa se posvetimo dražbi: radi bi sestavili slovar, katerega ključi bodo predmeti, vrednosti seznami ponudb za ta predmet. Spet bomo uporabili `defaultdict`, vendar vrednosti ne bodo `int`-i, temveč seznami. Funkcija `list` nam, če jo pokličemo brez argumentov, prikladno vrne prazen seznam.

    d = defaultdict(list)

    d["foo"]

    []

V tak seznam lahko celo dodajamo z `append`!

    d["bax"].append(12)
    d["bax"].append(5)

    d

    defaultdict(list, {'foo': [], 'bax': [12, 5]})

Zdaj pa zares.

    ponudbe = defaultdict(list)

    for vrstica in open("../domace-naloge/03-drazba-brez-anonimnosti/zapisnik.txt"):
        predmet, oseba, cena = vrstica.split(",")
        ponudbe[predmet].append(int(cena))

    for predmet, cene in ponudbe.items():
        print(predmet, cene)

    slika [31, 33, 35, 37, 40, 45]
    pozlačen dežnik [29]
    Meldrumove vaze [44, 46, 48, 53, 57, 60, 61, 63, 67, 71, 76, 78]
    skodelice [50, 55, 60, 61, 62, 65, 68, 70, 74, 76, 80, 83]
    kip [30, 32, 37, 39, 43, 44, 45, 50, 53, 55, 58, 61, 63, 68, 72, 76, 77, 81, 85, 86, 90, 92, 94, 97, 98, 99, 100, 103, 107]
    čajnik [15]
    srebrn jedilni servis [27, 30, 35, 39, 40, 45, 47, 49, 53, 55, 58, 59, 62, 63]
    perzijska preproga [16, 21]

Kako lažje bi bile domače naloge, če bi že pred par tedni vedeli za `defaultdict`! Vendar se potem nikoli ne bi naučili zares delati z datotekami, zankami, seznami... ;)

### `Counter`

`Counter` iz nekega razloga pišemo z veliko. (Razlog ni posebej dober. Iz istega razloga bi lahko z veliko pisali tudi `defaultdict`.

`Counter` je v sorodu z `defaultdict`-om in ga včasih nadomešča. Trenutno nimamo pri roki dobrega primera za njegovo uporabo, oziroma, točneje, da bi ga učinkovito uporabljali, bi morali znati nekaj, česar še ne znamo. Vseeno pa lahko pokažemo, kaj počne.

Recimo, da imamo seznam imen oseb, ki jim je nekdo telefoniral.

    klici = ["Ana", "Ana", "Berta", "Cilka", "Ana", "Cilka", "Dani", "Ema"]

Seveda želimo prešteti, kolikokrat je poklical koga. To bi znali kar trivialno narediti z `defaultdict`-om, s `Counter` pa je še trivialneje:

    from collections import Counter

    stevci = Counter(klici)

    stevci

    Counter({'Ana': 3, 'Cilka': 2, 'Berta': 1, 'Dani': 1, 'Ema': 1})

Pogosto nas bo zanimalo, koga je klical največkrat - ali pa katere tri - in za to obstaja metoda.

    stevci.most_common(3)

    [('Ana', 3), ('Cilka', 2), ('Berta', 1)]

Tole je seznam, urejen po pogostosti.

## Modul `csv`

Tale vam bo všeč. Datoteke, ki vsebujejo podatke, ločene z vejicami, tako kot jih je naš `zapisnik.txt`, so kar pogoste. Tej obliki zapisa rečemo comma separated values ali, s kratico, csv. V to obliko zna shranjevati tudi Excel - ob opozorilu, da se bo ob tem izgubilo vse oblikovanje in še kaj. (Enkrat sem obljubil, da se bomo učili brati Excelove datoteke. To še ni to. Brali bomo tudi .xlsx. Ampak še ne.) V tej obliki je bil tudi naš zapisnik dražbe in vse druge datoteke.

Python ima zato modul csv, ki zna brati take reči.

### `reader`

Nas zanimajo imena vseh udeležencev dražbe?

    import csv

    udelezenci = set()
    for predmet, oseba, cena in csv.reader(open("../domace-naloge/04-analiza-drazbe/zapisnik.txt")):
        udelezenci.add(oseba)

    udelezenci

    {'Ana', 'Berta', 'Cilka', 'Dani', 'Ema', 'Fanči', 'Greta', 'Helga'}

Funkciji `csv.reader` podamo datoteko (ne le imena, potrebno je poklicati tudi `open`). Vrne nekaj, čez kar lahko gremo z zanko `for`, pa dobivamo podatke iz vrstic. Nobenega `split`.

`csv.reader` privzame, da so podatki ločeni z vejico. Če so s čim drugim, mu to povemo z dodatnim argumentom `delimiter`. Ločilo v datoteki "kolesa.txt" iz pete domače naloge je bil `-`.

    ime_dat = "../domace-naloge/05-druzabno-omrezje-drazbe/kolesa.txt"

    for v in csv.reader(open(ime_dat), delimiter="-"):
        print(v)

    ['Cube', '5031', '159', 'Janez', '2017']
    ['Stevens', '3819', '1284', 'Ana', '2012']
    ['Focus', '3823', '1921', 'Benjamin', '2019']

Poleg ločila ima datoteka lahko še kup drugih lastnosti. Recimo, da na dražbi prodajajo komplet žlica, nož in vilice Ludvika XIV. V datoteki bi tako dobili vrstico

    žlica, nož in vilice Ludvika XIV,Ana,12945

in `s.split(",")` bi vrnil štiri stvari, namesto treh, pač zaradi vejice med žlico in nožem. Excel, recimo, bi v tem primeru napisal nekaj v slogu

    "žlica, nož in vilice Ludvika XIV",Ana,12945

S tem, ko bi zaprl prvo polje v narekovaje, bi povedal, da gre tu za eno samo reč in da je potrebno vejico znotraj tega ignorirati. Različni programi in sistemi imajo različna pravila; temu se v jeziku funkcije `csv.reader` reče `dialect`. Privzeti dialekt je `"excel"` in ta bo za datoteke iz Excela navadno delal. Za ostale lahko uporabite `Sniffer`.

    sniffer = csv.Sniffer()

Z metodo `.read()` preberemo celotno vsebino datoteke.

    open(ime_dat).read()

    'Cube-5031-159-Janez-2017\nStevens-3819-1284-Ana-2012\nFocus-3823-1921-Benjamin-2019\n'

`sniffer` ima metodo `sniff`, ki ji kot argument damo vsebino datoteke (ali vsaj dovolj velik kos, da je iz njega razvidno, kako je datoteka oblikovana). Sniffer bo uganil, v kakšnem slogu je napisana datoteka.

    dialect = sniffer.sniff(open(ime_dat).read())

Nato pokličemo `csv.reader` in mu poleg datoteke podamo še dialekt.

Da bo lažje vidno, naredimo vse še enkrat.

    import csv

    ime_dat = "../domace-naloge/05-druzabno-omrezje-drazbe/kolesa.txt"
    sniffer = csv.Sniffer()  # vrne novega "snifferja"
    dialect = sniffer.sniff(open(ime_dat).read())  # preberemo vsebino datoteke in jo damo posniffati :)
    for v in csv.reader(open(ime_dat), dialect):
        print(v)

    ['Cube', '5031', '159', 'Janez', '2017']
    ['Stevens', '3819', '1284', 'Ana', '2012']
    ['Focus', '3823', '1921', 'Benjamin', '2019']

### `DictReader`

Recimo, da bi imeli takšno datoteko s kolesi:

    kolo,razdalja,višina,lastnik,leto nakupa
    Cube,5031,159,Janez,2017
    Stevens,3819,1284,Ana,2012
    Focus,3823,1921,Benjamin,2019

Za razliko od prejšnjih datotek (in vseh, ki smo jih videli doslej) ima ta datoteka v prvi vrstici imena stolpcev. Zato jo lahko namesto z `reader` beremo z `DictReader`, ki za vsako vrstico ne vrne seznama temveč slovar, katerega ključi so imena stolpcev. To je imenitno.

    import csv

    for vrstica in csv.DictReader(open("kolesa-z-glavo.txt")):
        print(vrstica["kolo"], ":", vrstica["leto nakupa"])

    Cube : 2017
    Stevens : 2012
    Focus : 2019

Ker bodo imele vaše datoteke zelo pogosto glavo, boste pretežno uporabljali `DictReader`. To je praktično, saj je s slovarji lažje delati kot s seznami. Imena stolpcev, kot so kolo, lastnik in leto nakupa je lažje brati kot indekse 0, 3 in 4, pa še zmotili se ne bomo pri štetju.

## Modul `statistics`

O modulu `statistics` se ne bomo preveč razgovorili. Osebno ga nikoli ne uporabljam, saj se vse to in še veliko več najde v modulih knjižnice `numpy`, s katero se da tudi veliko preprosteje narediti veliko več - a le če znaš. Python ima ta modul zgolj zato, ker je `numpy` pač ogromna dodatna knjižnica, ki je ne dobimo s Pythonom. (Po drugi strani pa si vsak resen uporabnik pythona namesti tudi `numpy`.)

Skratka: `statistics` vsebuje funkcije `mean`, `median`, `mode`, `stdev` in še kup drugih, ki znajo za podani seznam izračunati poprečje, mediano, modus, standardno deviacijo in še kup drugega.

    import statistics

    visine = [185, 192, 160, 173, 180]

    statistics.mean(visine)

    178

    statistics.stdev(visine)

    12.227019260637483

Ve tudi za korelacijo in linearno regresijo, tu pa se neha. Kdor bi rad izvedel več, naj pogleda v dokumentacijo modula `statistics`.

## Moduli `time`, `datetime`, `calendar`

Pri delu boste najbrž pogosto naleteli na razne datume, čase in podobno. Funkcije, povezane s tem, so razmetane po treh modulih. Vse skupaj je prilična zmešnjava. Zanjo niti ni toliko kriv Python, kot, predvsem, dejstvo, da se tu navezuje na različne standardizirane funkcije različnih sistemov. Nekatere stvari na Windows delujejo drugače kot na macOS ali na Linuxu in potem dobimo, kar imamo.

### `time`

Modul `time` (dokumentacija) se ukvarja s trenutnim časom in drugimi časi in pretvarjanjem med njimi.

Glavna funkcija je `time`, ki vrne število sekundo, minevših od 1. januarja 1970 po Greenwichu.

    import time

    time.time()

    1707583016.433111

To je uporabno, če bi radi merili (realen) čas, ki je minil med dvema dogodkoma v programu. Vendar ... no, ne zelo. Za to obstajajo boljše funkcije.

Za naše potrebe sta morda bolj uporabni `gmtime` in `localtime`.

    print(time.gmtime())
    print(time.localtime())

    time.struct_time(tm_year=2024, tm_mon=2, tm_mday=10, tm_hour=16, tm_min=36, tm_sec=56, tm_wday=5, tm_yday=41, tm_isdst=0)
    time.struct_time(tm_year=2024, tm_mon=2, tm_mday=10, tm_hour=17, tm_min=36, tm_sec=56, tm_wday=5, tm_yday=41, tm_isdst=0)

Obe vrnete čudo istega tipa. Imenuje se `struct_time`. Tako ime kot oblika izhajata iz jezika C. Ime ni pomembno, pomembna so imena polj, ki vsebujejo trenutno leto, mesec in dan, uro, minute in sekunda, pa dan v tednu in letu, za zraven pa še polje, ki pove, ali gre za letni čas (1) ali ne (0).

    zdaj = time.localtime()

    print("Danes je ", zdaj.tm_mday, ". ", zdaj.tm_mon, ". ", zdaj.tm_year, ".", sep="")

    Danes je 10. 2. 2024.

Modul vsebuje tudi funkcijo za oblikovanje datumov. Uporabimo jo takole:

    time.strftime("Danes je %d. %m. %Y, ura pa je %H.%M", zdaj)

    'Danes je 10. 02. 2024, ura pa je 17.36'

Podamo ji niz, ki vsebuje znake, kot so `%d`, `%m`, `%Y` in tako naprej, ter čas. Funkcija bo te znake zamenjala z ustreznimi vrednostmi.

Celoten seznam možnih znakov najdete na https://docs.python.org/3/library/time.html#time.strftime.

    time.strftime("Danes je %A, %d. %B %Y", zdaj)

    'Danes je Saturday, 10. February 2024'

Tole ni izpadlo najboljše. Očitno se ni odločil za slovenščino. K temu bi se ga sicer dalo pripraviti.

    import locale

    locale.setlocale(locale.LC_ALL, "sl_SI")

    'sl_SI'

Zdaj pa bo.

    time.strftime("Danes je %A, %d. %B %Y", zdaj)

    'Danes je sobota, 10. februar 2024'

Tule smo mimogrede uporabili še module `locale`, ki vsebuje vse živo povezano z različnimi navadami različnih jezikov - od imen dni in mesecev, do tega, ali uporabljajo decimalno vejico ali piko in načina zapisa valut

    locale.currency(42.19)

    '42,19 SIT'

(haha, tolarji!)

### `datetime`

Bistvo modula `datetime` (dokumentacija) je manipulacija z datumi. Z njim lahko odštejete dva časa in izveste, koliko let, mesecev, dni, ur, minut, sekund je med njima. Ali pa pripravite objekt vrste `timedelta`, ki bo, recimo, dve meseca in tri dni, ter to prištejete k nekemu datumu.

Še pomembneje: modul zna pretvarjati čase iz nizov.

    from datetime import datetime

    datetime.strptime("13. november 2023, 21:14", "%d. %B %Y, %H:%M")

    datetime.datetime(2023, 11, 13, 21, 14)

Najprej čudni import: iz modula `datetime` uvozimo "funkcijo" `datetime`. (V resnici ni funkcija, temveč tip, vendar za tole zdaj ni časa. :)

Funkcija `strptime` zahteva niz, v katerem je zapisan datum, in niz, ki pove obliko niza - spet s črkami, kot smo jih videli v `strftime`. Do delov rezultata spet dostopamo z imeni polj.

    cas = datetime.strptime("13. november 2023, 21:14", "%d. %B %Y, %H:%M")

    cas.day

    13

    cas.hour

    21

Tole vam bo znalo razkopati datum v prilično poljubni obliki. Takole opravimo z Američani.

    datetime.strptime("11/13/2023", "%m/%d/%Y")

    datetime.datetime(2023, 11, 13, 0, 0)

Takole pa z Američani, ki pišejo leto brez stoletij.

    datetime.strptime("11/13/23", "%m/%d/%y")

    datetime.datetime(2023, 11, 13, 0, 0)

### `calendar`

Zadnji od treh modulov, povezanih s časom, je preprosti `calendar` (dokumentacije). Ta vsebuje stvari kot so imena dni, mesecev in podobno.