Seznami & nabori#
Če želimo delati z zaporedjem podatkov, uporabimo sezname. 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.
Osnovne operacije na seznamih#
Precej operacij na seznamih je enakih kot na nizih. Sezname lahko stikamo z operacijo +
in množimo s celimi števili:
[True, False] + [True]
[True, False, True]
3 * ['x', 'y']
['x', 'y', 'x', 'y', 'x', 'y']
Dolžino seznama dobimo s funkcijo len
:
len([100, 200, 300])
3
len([])
0
Sezname med seboj primerjamo leksikografsko: najprej prvi par, če sta ta dva enaka, naslednji par in tako naprej.
[1, 100, 10000] < [2, 0, 0]
True
Prav tako imamo na voljo predikata in
in not in
, s katerima ugotovimo, ali se nek element pojavlja v seznamu:
prastevila = [2, 3, 5, 7, 11, 13, 17]
5 in prastevila
True
15 not in prastevila
True
Predikat in
na seznamih se razlikuje od tistega na nizih v tem, da preverja le pojavitev elementov, ne pa podseznamov:
[3, 5, 7] in prastevila
False
Na primer:
def stevilo_dni(mesec, leto):
if mesec == 2:
return 29 if je_prestopno(leto) else 28
elif mesec in [4, 6, 9, 11]:
return 30
else:
return 31
Indeksiranje in rezine na seznamih delujejo tako kot na nizih:
prastevila = [2, 3, 5, 7, 11, 13, 17]
prastevila[2]
5
prastevila[-2]
13
prastevila[len(prastevila) - 2]
13
prastevila[2:-2]
[5, 7, 11]
prastevila[::2]
[2, 5, 11, 17]
Če imamo gnezdene sezname, do elementov dostopamo z gnezdenimi indeksi:
mat = [[1, 0, 0], [0, -1, 2], [3, 1, 5]]
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).
Zanke 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:
for x in [10, 20, 30]:
print(x)
10
20
30
Na primer, vsoto vseh elementov seznama bi definirali kot:
def vsota_elementov(seznam):
"""Vrne vsoto elementov v danem seznamu."""
vsota = 0
for element in seznam:
vsota += element
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 element in seznam:
if element > najvecji_do_zdaj:
najvecji_do_zdaj = element
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#
Za razliko od nizov lahko vrednosti v seznamih tudi spreminjamo:
sez = [10, 20, 30]
sez[1]
20
sez[1] = 40
sez
[10, 40, 30]
Zamenjamo lahko tudi celotno rezino
sez = [10, 20, 30, 40]
sez[1:3]
[20, 30]
[20, 30]
sez[1:3] = [0, 0, 0]
sez
[10, 0, 0, 0, 40]
Če nadomestna rezina ni enake dolžine kot prvotna, se seznam ustrezno skrajša ali podaljša. 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 = [10, 20, 30, 40]
sez[2:2] = [0, 0, 0]
sez
[10, 20, 0, 0, 0, 30, 40]
sez = [10, 20, 30, 40]
sez[2] = [0, 0, 0]
sez
[10, 20, [0, 0, 0], 40]
Tako elemente kot rezine lahko tudi brišemo
sez = [10, 20, 30, 40, 50]
del sez[1]
sez
[10, 30, 40, 50]
sez = [10, 20, 30, 40, 50]
del sez[2:4]
Pri spreminjanju seznamov je treba biti previden, saj ne deluje tako, kot smo navajeni pri spreminjanju vrednosti spremenljivk. Na primer, če pišemo
a = 5
b = a
a = 0
b
5
Vidimo, da se vrednost spremenljivke b
ni spremenila, ko smo spremenili a
, saj smo jo v drugi vrstici nastavili na število 5. Pri seznamih je stvar malo drugačna. Če pišemo
a = [1, 1, 1]
b = a
a = [2, 2, 2]
b
[1, 1, 1]
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, 1, 1]
b = a
a[1] = 2
b
[1, 2, 1]
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] = 2
smo povedali, naj se na mesto 1
v seznamu, shranjenem v a
, zapiše 2. Ker je v b
shranjen isti (ne le enak) seznam kot v a
, je s tem tudi seznam v b
drugačen. Da je zmeda večja, +=
ni samo okrajšava za =
in +
, kot smo navajeni pri številih:
a = [1, 1, 1]
b = a
a = a + [2, 2, 2]
b
[1, 1, 1]
a = [1, 1, 1]
b = a
a += [2, 2, 2]
b
[1, 1, 1, 2, 2, 2]
V prvem primeru izračunamo nov seznam a + [2, 2, 2]
in ga shranimo pod ime a
, s čimer se b
ne spremeni. V drugem primeru pa je +=
operacija, ki razširi obstoječi seznam. Ker gre pri a
in b
za isti seznam, se spremeni tudi seznam v b
.
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.
Vgrajene metode na seznamih#
Kot pri nizih za večino pogosto uporabljanih stvari na seznamih obstajajo že vgrajene metode. Te povečini ne vračajo ničesar, temveč le spremenijo dani seznam. Izjemi sta metodi index
in count
, ki vrneta vrednost in seznam pustita pri miru, ter metoda pop
ki tako spremeni seznam kot vrne vrednost. Vse metode so naštete v uradni dokumentaciji, najbolj osnovne izmed njih pa so:
sez.append(x)
na konec seznamasez
doda elementx
.
sez = [10, 20, 30]
sez.append(40)
sez
[10, 20, 30, 40]
Metodo append
pogosto uporabljamo za izračun seznama ustreznih elementov. To storimo tako, da ustvarimo prazen seznam, nato pa vanj z metodo append
dodamo vsak ustrezen element. To je podoben postopek kot pri izračunu vsote ustreznih elementov, kjer smo ustvarili spremenljivko z začetno vrednostjo 0, nato pa ji prištevali ustrezne elemente.
sez.extend(sez2)
na konec seznamasez
doda vse elemente iz seznamasez2
.
sez = [10, 20, 30]
sez.extend([40, 50])
sez
[10, 20, 30, 40, 50]
Okrajšava +=
, ki smo jo videli prej, pokliče natanko metodo extend
.
sez = [10, 20, 30]
sez += [40, 50]
sez
[10, 20, 30, 40, 50]
sez.insert(i, x)
pred element na mestui
v seznamusez
vstavi elementx
.
sez = [10, 20, 30]
sez.insert(1, 0)
sez
[10, 0, 20, 30]
[10, 0, 20, 30]
sez.remove(x)
iz seznamasez
odstrani prvo pojavitev vrednostix
.
sez = [1, 2, 1, 2, 3]
sez.remove(2)
sez
[1, 1, 2, 3]
sez.pop(i=-1)
vrne element na mestui
v seznamusez
in odstrani ta element iz seznama. Če indeksai
ne podamo, metoda odstrani zadnji element.
sez = [10, 20, 30]
sez.pop()
30
sez
[10, 20]
sez.clear()
iz seznamasez
pobriše vse elemente.
sez = [1, 2, 1, 2, 3]
sez.clear()
sez
[]
sez.index(x)
vrne prvo mesto, na katerem se v seznamusez
nahaja vrednostx
.sez.count(x)
vrne število pojavitev vrednostix
v seznamusez
.sez.sort(key=None, reverse=False)
na mestu uredi seznam glede na vrednosti funkcijekey
. Če parameterreverse
nastavimo naTrue
, bo seznam urejen padajoče.
sez = [4, 1, 8, 2, 16]
sez.sort()
sez
[1, 2, 4, 8, 16]
Metoda spremeni obstoječi seznam. Če želimo namesto tega dobiti nov, urejen seznam, pa lahko pokličemo funkcijo sorted
, ki svojega argumenta ne spremeni:
sez = [4, 1, 8, 2, 16]
sorted(sez)
[1, 2, 4, 8, 16]
sez
[4, 1, 8, 2, 16]
sez.reverse()
obrne seznamsez
na glavo.
sez = [1, 2, 4, 8, 16]
sez.reverse()
sez
[16, 8, 4, 2, 1]
Tudi tu lahko s pomočjo rezin ustvarimo nov seznam, ki je obrnjen na glavo, starega pa pustimo pri miru::
sez = [1, 2, 4, 8, 16]
sez[::-1]
[16, 8, 4, 2, 1]
sez
[1, 2, 4, 8, 16]
Izpeljani seznami#
Sezname dostikrat lepše zgradimo z izpeljanimi seznami, ki so oblike [izraz for spremenljivka in mozne_vrednosti]
. Podobno kot v matematiki množice pišemo kot \({ 2 \cdot n \mid n \in {1, \dots, 9}}\), lahko v Pythonu napišemo seznam:
[2 * n for n in range(1, 10)]
[2, 4, 6, 8, 10, 12, 14, 16, 18]
ali pa na primer:
potence = [2 ** n for n in range(10)]
potence
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
[n - 1 for n in potence]
[0, 1, 3, 7, 15, 31, 63, 127, 255, 511]
[int(stevka) for stevka in str(3141592)]
[3, 1, 4, 1, 5, 9, 2]
Če želimo, lahko v izpeljani seznamih oblike [izraz for spremenljivka in mozne_vrednosti if pogoj]
s pogojem določimo, katere elemente želimo:
[2 * n for n in range(1, 10) if n % 3 == 1]
[2, 8, 14]
def vsota_pozitivnih_elementov(seznam):
"""Vrne seznam vseh pozitivnih elementov danega seznama."""
return sum([element for element in seznam if element > 0])
vsota_pozitivnih_elementov([10, -20, 50])
60
Zanke v izpeljanih seznamih so lahko gnezdene na dva načina. Prvi je, da kot elemente izpeljanega seznama spet podamo izpeljane sezname. Na primer, identično matriko lahko naredimo kot:
def identicna_matrika(n):
return [[1 if i == j else 0 for j in range(n)] for i in range(n)]
Lahko pa v izpeljanem seznamu naštejemo več zank, pri čemer ob vsakem koraku v obhodu prve spremenljivke naredimo obhod vseh preostalih:
def razlicni_pari(n):
return [(i, j) for i in range(n) for j in range(i, n)]
razlicni_pari(5)
[(0, 0),
(0, 1),
(0, 2),
(0, 3),
(0, 4),
(1, 1),
(1, 2),
(1, 3),
(1, 4),
(2, 2),
(2, 3),
(2, 4),
(3, 3),
(3, 4),
(4, 4)]
Izpeljani seznami pa niso vedno rešitev, tudi če so krajši. Na primer, če je vsaka vrednost v seznamu odvisna od prejšnjih, jih ne moremo uporabiti.
Nabori#
Nabori so prav tako kot seznami sestavljeni iz več elementov, pišemo pa jih med običajne oklepaje: (1, 2, 3)
. Nabor z enim elementom pišemo kot (1, )
(razmislite, zakaj ga ne pišemo kot (1)
). Namen naborov je hranjenje heterogenih podatkov. To pomeni, da imajo elementi na različnih mestih različne pomene. Na primer, če vemo da vsako število v seznamu [103, 111, 98, 106]
predstavlja rezultat neke meritve, bi podobno sklepali tudi, če bi v seznam dodali še kakšno drugo številko. Pri datumu (25, 6, 1991)
pa imajo različne komponente različne pomene: prva predstavlja dan, druga mesec in tretje leto. Če bi dodali še četrto številko, bi morali povedati, kaj pomeni. Python nam seveda omogoča, da bi meritve zapisali kot (103, 111, 98, 106)
, datum pa kot [25, 6, 1991]
, vendar bomo sezname uporabljali za poljubno število podatkov z enakim pomenom, nabore pa za fiksno število podatkov z različnimi pomeni.
S tem namenom so nabori v Pythonu nespremenljivi:
x = (1, 1, 1)
x[2] = 2
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[70], line 2
1 x = (1, 1, 1)
----> 2 x[2] = 2
TypeError: 'tuple' object does not support item assignment
Sicer pa za nabore veljajo podobne lastnosti kot za sezname: 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.
(True, False) + (True,)
(True, False, True)
3 * ('x', 'y')
('x', 'y', 'x', 'y', 'x', 'y')
5 in (1, 2, 3)
False
(1, 2, 5, 10, 20, 50)[3]
10
(1, 2, 5, 10, 20, 50)[3:]
(10, 20, 50)
Če imamo nabor, ga lahko razstavimo na posamezne spremenljivke, na primer
datum = (25, 6, 1991)
dan, mesec, leto = datum
f'{dan}. {mesec}. {leto}'
'25. 6. 1991'
V resnici gre pri hkratnem prireditvenem stavku kot je
x, y = 10, 20
za to, da najprej naredimo torej nabor z dvema komponentama oziroma par (10, 20)
, nato pa ga razstavimo na dve spremenljivki.
Funkciji enumerate
in zip
#
Dostikrat želimo hkrati dostopati do elementov seznama in njihovih indeksov.
Predstavimo polinome s seznamom koeficientov, urejenim od prostega proti vodilnemu členu. Polinom \(3 - x^2\) bi tako predstavili s seznamom [3, 0, -1]
. Pri izračunu vrednosti polinoma želimo hkrati dostopati tako do koeficientov kot do njihovih indeksov, ki ustrezajo potenci. To lahko storimo na več načinov. Lahko se vozimo po indeksih in prek njih dostopamo do koeficientov:
def vrednost_polinoma(polinom, x):
vsota = 0
for i in range(len(polinom)):
koef = polinom[i]
vsota += koef * x ** i
return vsota
vrednost_polinoma([3, 0, 1], 1)
4
vrednost_polinoma([3, 0, 1], 2)
7
Lahko se vozimo po koeficientih in hkrati povečujemo števec indeksa:
def vrednost_polinoma(polinom, x):
vsota = 0
i = 0
for koef in polinom:
vsota += koef * x ** i
i += 1
return vsota
Warning
Paziti moramo, da indeksa ne računamo s pomočjo metode index
, saj ta vrne indeks prve pojavitve iskane vrednosti, kar je narobe (pa še počasno):
```{code-cell}
def napacna_vrednost_polinoma(polinom, x):
vsota = 0
for koef in polinom:
i = polinom.index(koef)
vsota += koef * x ** i
return vsota
```{code-cell}
vrednost_polinoma([0, 2, 0, 2], 3)
```
```{code-cell}
napacna_vrednost_polinoma([0, 2, 0, 2], 3)
```
Ker je v spodnjem klicu funkcije metoda `index` za indeks prve pojavitve vrednosti 2 obakrat vrnila 1, je funkcija vrnila $2 \cdot 3^1 + 2 \cdot 3^1 = 6$ namesto $2 \cdot 3^1 + 2 \cdot 3^3 = 60$.
Najbolj enostavno pa je, da uporabimo funkcijo `enumerate`, ki zaporedoma vrača pare, v katerih so druge komponente vrednosti danega seznama, prve komponente pa njihovi indeksi:
```{code-cell}
for x in enumerate('abc'):
print(x)
Pare, ki nam jih podaja enumerate
lahko v dve spremenljivki razstavimo tudi v zanki for
:
for i, x in enumerate('abc'):
print(i, x)
0 a
1 b
2 c
S pomočjo funkcije enumerate
lahko vrednost polinoma izračunamo kot:
def vrednost_polinoma(polinom, x):
vsota = 0
for i, koef in enumerate(polinom):
vsota += koef * x ** i
return vsota
ali še krajše z izpeljanim seznamom:
def vrednost_polinoma(polinom, x):
return sum([koef * x ** i for i, koef in enumerate(polinom)])
Podobno kot enumerate
deluje funkcija zip
, ki sprejme več seznamov, vrne pa zaporedje naborov istoležnih elementov:
for x in zip('xyz', [10, 20, 30], [4, 5, 6]):
print(x)
('x', 10, 4)
('y', 20, 5)
('z', 30, 6)
Funkciji se reče zip
, ker združuje elemente različnih seznamov tako kot zadrga. Vrnjeno zaporedje ima toliko elementov, kot najkrajši argument funkcije:
for x in zip('xyz', [10, 20, 30, 40]):
print(x)
('x', 10)
('y', 20)
('z', 30)
S pomočjo funkcije zip
lahko enostavno izračunamo skalarni produkt:
def skalarni_produkt(vektor1, vektor2):
"""Vrne skalarni produkt dveh vektorjev iste dimenzije."""
assert len(vektor1) == len(vektor2)
vsota = 0
for x1, x2 in zip(vektor1, vektor2):
vsota += x1 * x2
return vsota
ali kot:
def skalarni_produkt(vektor1, vektor2):
"""Vrne skalarni produkt dveh vektorjev iste dimenzije."""
assert len(vektor1) == len(vektor2)
return sum([x1 * x2 for x1, x2 in zip(vektor1, vektor2)])
skalarni_produkt([1, -2, 5], [-2, 5, 2])
-2