Seznami

Če želimo delati z zaporedjem podatkov, uporabimo sezname.

Delo s seznami

Pisanje seznamov

Sename pišemo v oglatih oklepajih, med katerimi napišemo vrednosti, ločene z vejicami, na primer [10, 20, 30] je seznam, ki vsebuje tri števila, [] pa prazen seznam. Če želimo, lahko vejico pišemo tudi za zadnjim elementom. Seznami so lahko tudi gnezdeni. Na primer, matriko bi predstavili s seznamom seznamov:

[[1, 2, 3],
 [4, 5, 6],
 [7, 8, 9]]

V sezname lahko spravimo vrednosti različnih tipov, na primer:

[1, True, [2, 5], "Niz", 3.14]

Vendar običajno sezname uporabimo za predstavitev homogene zbirke podatkov, torej da so vse vrednosti istega tipa.

Operacije na seznamih

Tako kot nize lahko tudi sezname stikamo z operacijo + in množimo s celimi števili:

>>> [10, 20, 30] + [6, 5, 4]
[10, 20, 30, 6, 5, 4]
>>> 4 * [1, 2]
[1, 2, 1, 2, 1, 2, 1, 2]

Dolžino seznama dobimo s funkcijo len:

>>> len([100, 200, 300])
3
>>> len([])
0

Tudi na seznamih imamo na voljo predikata in in not in, s katerima ugotovimo, ali se nek element pojavlja v seznamu:

>>> 'Ema' in ['Ana', 'Bojan', 'Cvetka', 'David']
False
>>> 'Ana' in ['Ana', 'Bojan', 'Cvetka', 'David']
True
def stevilo_dni(mesec, leto):
    if mesec == 2:
        return 29 if je_prestopno(leto) else 28
    elif mesec in [1, 3, 5, 7, 8, 10, 12]:
        return 31
    elif mesec in [4, 6, 9, 11]:
        return 30

Indeksiranje in rezine

Indeksiranje in rezine na seznamih delujejo tako kot na nizih:

>>> sez = [5, 3, 8, 2, 5, 2, 1, 2]
>>> sez[0]
5
>>> sez[2]
8
>>> sez[len(sez) - 1]
2
>>> sez[-1]
2
>>> sez[:2]
[5, 3]
>>> sez[1:3]
[3, 8]
>>> sez[3:]
[2, 5, 2, 1, 2]
>>> sez[1:5:2]
[3, 2]
>>> sez[::2]
[5, 8, 5, 1]

Če imamo gnezdene sezname, do elementov dostopamo z gnezdenimi indeksi:

>>> mat = [[1, 0, 0], [0, -1, 2], [3, 1, 5]]
>>> mat[0][0]
1
>>> mat[1][-1]
2

Na primer, sled matrike bi lahko izračunali kot:

def sled(matrika):
    '''Izračuna sled dane matrike.'''
    vsota_diagonalnih = 0
    for i in range(len(matrika)):
        vsota_diagonalnih += matrika[i][i]
    return vsota_diagonalnih
>>> mat = [[5]]
>>> sled(mat)
5

Sledi pa nikakor ne bomo izračunali na sledeči (pri študentih dostikrat videni) način:

def grozna_sled(matrika):
    '''Na popolnoma napačen izračuna sled dane matrike.'''
    vsota_diagonalnih = 0
    for i in range(len(matrika)):
        for j in range(len(matrika)):
            if i == j:
                vsota_diagonalnih += matrika[i][j]
    return vsota_diagonalnih

Funkcija sled matrike sicer izračuna pravilno, vendar na izjemno potraten način, saj se sprehodi čez celotno matriko, ne le čez diagonalne elemente. Na primer, pri matriki velikosti \(1000 \times 1000\) bi druga funkcija pregledala tisočkrat več elementov (in posledično porabila tisočkrat več časa).

Zanka for na seznamih

Tako kot se lahko z zanko for sprehodimo po vseh znakih v nizu, se lahko z njo sprehodimo tudi po vseh elementih danega seznama:

def vsota_elementov(seznam):
    '''Vrne vsoto elementov v danem seznamu.'''
    vsota = 0
    for trenutni in seznam:
        vsota += trenutni
    return vsota
>>> vsota_elementov([10, 2, 4000, 300])
4312

Največji element v danem seznamu lahko poiščemo tako, da zaporedoma vsak element seznama primerjamo z do sedaj največjim videnim elementom. Če je trenutni element večji, do sedaj največji element popravimo. Ko pregledamo vse elemente v seznamu, je do sedaj največji element tudi na splošno največji element. Edina stvar, na katero moramo še paziti, je ta, da na začetku izberemo ustrezen največji element. Tu imamo dve dobri izbiri. (Slaba izbira bi bila, da bi za največji do zdaj viden element vzeli neko dovolj majhno število, na primer 0 ali -9999999 – ta izbira je očitno napačna!) Prva dobra izbira je kar prvi element v seznamu, pri čemer moramo potem poprej preveriti še to, da je seznam neprazen:

def najvecji_element(seznam):
    '''Vrne največji element v danem seznamu. Če ga ni, vrne None'''
    if len(seznam) == 0:
        return
    najvecji_do_zdaj = seznam[0]
    for trenutni in seznam:
        if trenutni > najvecji_do_zdaj:
            najvecji_do_zdaj = trenutni
    return najvecji_do_zdaj

Druga izbira pa je None, vendar moramo potem pri vsaki primerjavi pogledati, ali imamo že “pravi” največji element ali je to do sedaj še vedno None:

def najvecji_element(seznam):
    '''Vrne največji element v danem seznamu. Če ga ni, vrne None'''
    najvecji_do_zdaj = None
    for trenutni in seznam:
        if najvecji_do_zdaj == None or trenutni > najvecji_do_zdaj:
            najvecji_do_zdaj = trenutni
    return najvecji_do_zdaj
>>> najvecji_element([10, 2, 4000, 300])
4000

Seveda lahko uporabimo tudi vgrajene funkcije:

>>> sum([10, 2, 4000, 300])
4312
>>> min([10, 2, 4000, 300])
2
>>> max([10, 2, 4000, 300])
4000

Spreminjanje seznamov

Spreminanje posameznih elementov

Za razliko od nizov lahko vrednosti v seznamih tudi spreminjamo:

>>> sez = [5, 3, 8, 2, 5, 7, 1, 2]
>>> sez[3] = 200
>>> sez
[5, 3, 8, 200, 5, 7, 1, 2]
>>> sez[-1] = 500
>>> sez
[5, 3, 8, 200, 5, 7, 1, 500]

Vrednosti lahko tudi brišemo

>>> del sez[5]
>>> sez
[5, 3, 8, 200, 5, 1, 500]

Spreminjanje rezin

Spreminjamo lahko tudi celotne rezine:

>>> sez[1:3]
[3, 8]
>>> sez[1:3] = [100, 300]
>>> sez
[5, 100, 300, 200, 5, 1, 500]

Če nadomestna rezina ni enake dolžine kot prvotna, se seznam ustrezno skrajša ali podaljša

>>> sez[2:5] = []
>>> sez
[5, 100, 1, 500]
>>> sez[2:2] = [0, 0, 0]
>>> sez
[5, 100, 0, 0, 0, 1, 500]

Kot vidimo, lahko nadomestimo tudi prazno rezino, s čimer nove elemente vrinemo v seznam. Nadomeščanje prazne rezine ni isto kot nadomeščanje elementa z istim indeksom kot rezina:

>>> sez[2] = [20, 20, 20]
>>> sez
[5, 100, [20, 20, 20], 0, 0, 1, 500]

Tudi rezine lahko brišemo:

>>> del sez[1:4]
>>> sez
[5, 0, 1, 500]

Previdnost pri spreminjanju seznamov

Pri spreminjanju seznamov je treba biti previden, saj ne deluje tako, kot smo navajeni pri spreminjanju vrednosti spremenljivk. Na primer, pišimo

>>> a = 5
>>> b = a
>>> a = 0
>>> b
5

Vidimo, da se vrednost spremenljivke b ni spremenila, saj smo jo v drugi vrstici nastavili na število 5. Pri seznamih je stvar malo drugačna. Če pišemo

>>> a = [1, 2, 3]
>>> b = a
>>> a = []
>>> b
[1, 2, 3]

so stvari še vedno take, kot bi jih pričakovali. Vrednost b smo nastavili na isti seznam kot a, vendar smo potem rekli, da naj bo v a shranjen drugačen seznam, s čimer na vrednost v b nismo vplivali. Če pa pišemo

>>> a = [1, 2, 3]
>>> b = a
>>> a[1] = 20
>>> b
[1, 20, 3]

se je s tem, ko smo spremenili a, spremenil tudi b. Kaj se je zgodilo? Ko smo napisali b = a, smo povedali, naj bo v b shranjen isti seznam kot a. In z a[1] = 20 smo povedali, naj se na mesto 1 v seznamu, shranjenem v a, zapiše 20. Ker je v b shranjen isti (ne le enak) seznam kot v a, je s tem tudi seznam v b drugačen.

Pogosta past, v katero se na začetku ujamemo zaradi spremenljivosti seznamov, je izračun identične matrike. Vemo že, da lahko v Pythonu seznam pomnožimo s številom:

>>> 3 * [0]
[0, 0, 0]

To nam da idejo, da bi lahko na isti način izračunali ničelno matriko:

>>> 3 * [3 * [0]]
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]

Izračun je videti pravilen, vendar vse tri vrstice te matrike kažejo na isti seznam. To je tako, kot če bi pisali:

>>> vrstica = [0, 0, 0]
>>> matrika = [vrstica, vrstica, vrstica]

Poskusimo iz te matrike dobiti identično matriko tako, da po diagonali nastavimo enice. Najprej nastavimo prvi element v prvi vrstici:

>>> matrika[0][0] = 1
>>> matrika
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]

Kaj se je zgodilo? Ker druga in tretja vrstica kažeta na isti seznam kot prva, smo tudi v njima prvi element popravili na 1. Če sedaj nastavimo še drugi element v drugi vrstici in tretjega v tretji vrstici se zgodba ponovi:

>>> matrika[1][1] = 1
>>> matrika[2][2] = 1
>>> matrika
[[1, 1, 1], [1, 1, 1], [1, 1, 1]]

Če želimo identično matriko izračunati na pravilen način, moramo za predstavitev vsake vrstice podati svoj seznam, zato ne moremo uporabiti le pomnoževanja seznamov.

Nabori

Nabori se obnašajo podobno kot seznami, le da njihovih vrednosti ne moremo spreminjati. Pišemo jih tako kot sezname, le med običajne oklepaje: (1, 2, 3). Nabor z enim elementom pišemo kot (1, ) (razmislite, zakaj ga ne pišemo kot (1)). Včasih lahko nabore pišemo kar brez oklepajev:

>>> 1, 2, 3
(1, 2, 3)

Druga razlika pa je ta, da so nabori običajno heterogeni: elementi na različnih mestih imajo lahko različne tipe in različne pomene:

student = ('Ana', 'Novak', 27162315)
ucenci = ['Ana', 'Bojan', 'Cvetka', 'David']
datum = (30, 'marec', 2016)
datumi = [(30, 'marec', 2016), (1, 'april', 2016), (25, 'junij', 2016)]

Sicer za nabore veljajo podobne lastnosti: lahko jih stikamo in množimo; lahko izračunamo njihovo vsoto, minimum, maksimum in dolžino; s predikatom in lahko pogledamo, ali vsebujejo dani element; lahko jih indeksiramo in delamo rezine; po njih se lahko sprehodimo z zanko for; od metod pa sta na voljo le count in index, saj ti dve edini ne spreminjata ničesar.

Razstavljanje naborov

Kljub temu, da lahko do elementov nabora dostopamo z indeksi, je pogosto uporabno, da jih razstavimo. To storimo s hkratnim prireditvenim stavkom, v katerem na levi strani naštejemo več spremenljivk, na desni pa podamo nabor, ki ga želimo razstaviti:

>>> datum = (30, 'marec', 2016)
>>> dan, mesec, leto = datum
>>> dan
30
>>> mesec
'marec'

V resnici gre pri hkratnih prireditvenih stavkih za sestavljanje in razstavljanje naborov. Pri razstavljanju je treba paziti, da je število spremenljivk na levi enako dolžini nabora na desni, saj v nasprotnem primeru pride do napake.

Razstavljanje seznamov

Na podoben način kot nabore lahko razstavljamo tudi sezname:

>>> prvi, drugi, tretji = [10, 20, 30]
>>> prvi
10
>>> tretji
30

Toda seznami običajno nimajo definirane dolžine, zato lahko pri njihovem razstavljanju uporabimo tudi poseben vzorec, ki v spremenljivko shrani vse preostale elemente:

>>> prvi, drugi, *ostali = [10, 20, 30, 40, 50, 60, 70]
>>> prvi
10
>>> drugi
20
>>> ostali
[30, 40, 50, 60, 70]

Vzorec za preostale elemente se lahko pojavi tudi kje drugje kot na koncu:

>>> prvi, *ostali, predzadnji, zadnji = [10, 20, 30, 40, 50, 60, 70]
>>> prvi
10
>>> ostali
[20, 30, 40, 50]
>>> predzadnji
60
>>> zadnji
70

Vseeno pa vzorec za preostale elemente lahko uporabimo največ enkrat:

>>> *prva_polovica, *druga_polovica = [1, 2, 3, 4]
Traceback (most recent call last):
  ...
SyntaxError: two starred expressions in assignment

Na podoben način lahko razstavljamo tudi nabore, nize in ostale strukture, po katerih se lahko sprehajamo z zanko for, vendar bo v spremenljivki vedno shranjen seznam preostalih elementov:

>>> zacetnica, *ostale_crke = 'abrakadabra'
>>> zacetnica
'a'
>>> ostale_crke
['b', 'r', 'a', 'k', 'a', 'd', 'a', 'b', 'r', 'a']
>>> prvi_par, *ostali_pari = enumerate('balon')
>>> prvi_par
(0, 'b')
>>> ostali_pari
[(1, 'a'), (2, 'l'), (3, 'o'), (4, 'n')]

Vzorec za preostale elemente lahko uporabimo tudi v definicijah funkcij:

def vrni_zadnji_argument(*args):
    return args[-1]
>>> vrni_zadnji_argument(10, 20, 30)
30
>>> vrni_zadnji_argument(1)
1

Tak vzorec na primer uporablja funkcija max (in njej podobne). Ta funkcija namreč deluje tako, da v primeru, ko dobi en argument, vrne njegov največji element, ko pa dobi več argumentov, pa vrne največjega:

>>> max([3, 5], [4, 1])
[4, 1]
>>> max([3, 5, 4, 1])
5
>>> max(3, 5, 4, 1)
5