3.1. Liste i drugi spremnici¶
3.1.1. Liste¶
Liste su središnji objekti programiranja u Pythonu. Srodne su poljima (array) iz standardnih programskih jezika (C, Fortran), ali imaju bitno više svojstava. Elementi lista mogu biti praktički bilo koji objekti.
>>> t1 = [1, 3, 5, 7, 9, 11]; t1
[1, 3, 5, 7, 9, 11]
>>> t2 = [[], ["jedan"], t1]; t2
[[], ['jedan'], [1, 3, 5, 7, 9, 11]]
Dakle, elementi liste u načelu mogu biti različitog tipa, kao u slučaju
gornje liste t2
, ali to je rijetko korisno.
Liste se imeđu ostalog mogu zbrajati, množiti i može im se mjeriti duljina:
>>> t1+t2
[1, 3, 5, 7, 9, 11, [], ['jedan'], [1, 3, 5, 7, 9, 11]]
>>> 7*[3]
[3, 3, 3, 3, 3, 3, 3]
>>> len(t1)
6
Kako je Python objektno orijentirani računalni jezik, mnoge operacije se izvode
putem tzv. metoda objekta, a to su objektu pridružene funkcije koje se
pozivaju sintaksom objekt.metoda()
. I liste su objekti pa imaju niz metoda
(vidi popis), od
kojih je daleko najvažnija metoda append()
koja služi za dodavanje
elemenata na kraj liste:
>>> t1.append(13); t1
[1, 3, 5, 7, 9, 11, 13]
Pojedinim elementima liste se pristupa pomoću indeksiranja, gdje prvi element liste ima indeks 0, drugi ima indeks 1 itd. Pojedine elemente možemo promijeniti običnim pridruživanjem pomoću znaka jednakosti:
>>> t1[1] = "tri"
>>> t1
[1, 'tri', 5, 7, 9, 11, 13]
Ukoliko želimo pristupiti većem broju elemenata od koristi su tzv. rezovi
liste (engl. slice). Općenita sintaksa reza je lista[početak:kraj:korak]
, gdje
je početak
uključen u rez, a kraj
nije:
>>> t1[2:6]
[5, 7, 9, 11]
>>> t1[2:6:2]
[5, 9]
Ukoliko rez ide od samog početka ili sasvim do kraja liste, odgovarajuće indekse možemo izostaviti:
>>> t1[2:]
[5, 7, 9, 11, 13]
Negativni indeksi nam omogućuju da brojimo od kraja liste ...
>>> t1[-2:] # zadnja dva elementa
[11, 13]
... a negativan korak da rez ide unatrag tj. da se izvrne redosljed elemenata:
>>> t1[::-1] # lista natraške
[13, 11, 9, 7, 5, 'tri', 1]
Zadatak 1
Promijenite u gornjoj listi t1 element "tri"
(ne element s indeksom=3!)
natrag u broj 3
, ali ne eksplicitnom upotrebom indeksa 1
, već tako
da “pronađete” indeks elementa "tri"
pomoću metode index()
.
(Nije dozvoljeno upotrijebiti znamenku 1
).
Za konstrukciju liste cijelih brojeva pogodna je konstrukcija
list(range(početak, kraj, korak))
gdje treba imati na umu da će konstruirana
lista početi s elementom početak
, ali će završiti s elementom kraj-1
.
>>> list(range(3)) # rezultira listom od tri elementa
[0, 1, 2]
range()
je niz, što je poseban tip Python objekta,
a list
je funkcija koja konvertira taj niz u listu.
Za konstrukciju liste realnih brojeva, vidi dolje odjeljak NumPy polja.
3.1.2. Obuhvaćanje liste¶
U praksi je kirurško mijenjanje pojedinih elemenata liste, kao u gornjem zadatku, vrlo rijetko. Listama se u pravilu rukuje kao cjelinama i najčešće se djeluje na sve njihove elemente. U standardnim računalnim jezicima u tu svrhu se koriste petlje, ali u Pythonu je najelegantnije koristiti tzv. obuhvaćanje liste (engl. list comprehension ):
>>> t3 = [1, 3, 7, 11]
>>> [n**2 for n in t3]
[1, 9, 49, 121]
Obuhvaćanjem range()
-a možemo konstruirati najrazličitije liste:
>>> from scipy import *
>>> pi
3.141592653589793
>>> [round(pi, k) for k in range(6)]
[3.0, 3.1, 3.14, 3.142, 3.1416, 3.14159]
>>> import numpy as np
>>> [np.random.random() for k in range(4)]
[0.3745401188473625, 0.9507143064099162, 0.7319939418114051, 0.5986584841970366]
Primijetite da varijablu “petlje” k
ne moramo eksplicitno koristiti pri
konstrukciji elemenata liste.
Često se javlja potreba za izborom elemenata liste koji zadovoljavaju neki kriterij. To se može izvesti obuhvaćanjem liste uz dodatni uvijet:
>>> t4 = range(1, 11)
>>> [n for n in t4 if (n % 2) == 0] # izbor parnih elemenata
[2, 4, 6, 8, 10]
(Kao i u jeziku C, operator %
daje ostatak cjelobrojnog dijeljenja.)
Napomena
Mjerenje vremena potrebnog da se izvrši neka ćelija izvodi se stavljanjem
%%time
u prvi red, a prekid računa koji traje predugo izvodi se putem
izbornika Kernel
pa Interrupt
ili pritiskom na ikonu Stop
u
traci s alatima.
%%time
naredba ne pripada jeziku Python već je to tzv.
“magic”
naredba koja modificira ponašanje IPython/Jupyter ćelije.
>>> import sympy
>>> %time sympy.factorint(1627222223994444443322222221)
CPU times: user 2.73 s, sys: 0 ns, total: 2.73 s
Wall time: 2.73 s
{10100000011: 1, 161111111111111111: 1}
Zadatak 2
Koristeći funkciju sympy.isprime
i obuhvaćanje liste, konstruirajte listu
svih prim-brojeva manjih od 100. Koliko ima prim-brojeva manjih od
\(10^4, 10^5, 10^6, \ldots\)? Usporedite rezultate (i brzinu njegovog
dobivanja) s funkcijom sympy.primepi().
Zadatak 3
Izračunajte funkciju \(\pi(x)\) (broj prim-brojeva manjih od x) za \(x=10^{10}\) statističkom metodom: Izaberite uzorak od n slučajnih cijelih brojeva između 1 i x i testirajte koliko ima prim-brojeva među njima. Iz tog udjela odredite \(\pi(x)\). Kolika veličina uzorka vam treba da bi relativna greška prema \(\pi(10^{10}) = 455052511\) bila otprilike 1% ili manje?
Zadatak 4
Kreirajte listu od 100 slučajnih brojeva iz intervala [0, 10)
,
a zatim u toj listi ostavite samo brojeve koji se za manje od
0.02
razlikuju od cijelog broja. Naputak: Testirajte
abs(x-round(x))
.
3.1.3. Ostali spremnici¶
Osim lista, postoje i drugi spremnici (containers) za objekte. Kao prvo tu su tzv tuplovi (tuple) koji “izvana” izgledaju isto kao liste samo u okruglim zagradama.
>>> tpl = (1, 3, 5, 7)
>>> tpl[3]
7
Glavna razlika prema listama je da su tuplovi nepromjenjivi. Nije moguće niti promijeniti neki njihov element niti im dodati nove:
>>> tpl[3] = 2
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
Razlika u upotrebi između tuplova i lista je da su tuplovi obično heterogeni strukturirani skupovi u kojima pojedina mjesta u tuplu nose različita značenja, dok su liste obično homogeni skupovi istovrsnih objekata. Npr, koordinate neke točke je prirodno staviti u tupl (x, y), a ne u listu [x,y], ali niz točaka je prirodno staviti u listu takvih tuplova [(x1, y1), (x2, y2), ...].
Kad varijablama pridružujemo vrijednosti iz kraćih lista ili tuplova zgodno je koristiti tehniku raspakiravanja
>>> x, y, z = (1, 2, 3)
>>> print("Norma vektora = {}".format(sqrt(x**2 + y**2 + z**2)))
Norma vektora = 3.7416573867739413
Raspakiravanje se često koristi kod procesiranja listi čiji su elementi liste ili tuplovi. U slijedećem primjeru pretvaramo listu 2D kartezijevih vektora u listu njihovih normi. Vidimo kako možemo kombinirati raspakiravanje po unutarnjoj s obuhvaćanjem 2D liste po vanjskoj dimenziji:
>>> [sqrt(x**2 + y**2) for x,y in [(1,2), (3,4), (5,6), (7,8)]]
[2.2360679774997898, 5.0, 7.810249675906654, 10.63014581273465]
Zadatak 5
Koristeći raspakiravanje tuplova u kombinaciji s obuhvaćanjem liste pretvorite ovu listu vektora
>>> tt = [(2.23, 1.11), (5.0, 0.93), (7.81, 0.88), (10.63, 0.85)]
iz 2D polarnog sustava u kartezijev:
Nakon toga transformirajte nastalu listu kartezijevih vektora u listu kartezijevih
vektora s cjelobrojnim koordinatama, zaokruživanjem vrijednosti x i y
koordinata pomoću funkcije round()
.
Daljnji spremnici koji nam stoje na raspolaganju su skupovi (set ), koji su skupovi različitih(!) objekata i s kojima možemo raditi standardne stvari poput unije, presjeka, komplementa ...
>>> s1 = set([10, 9, 10, 8, 7, 7, 7, 4]); s1
{8, 9, 10, 4, 7}
(Uočite da se duplikati automatski izbacuju.)
>>> s1.union(t1)
{1, 3, 4, 5, 7, 8, 9, 10, 11, 13}
>>> s1.intersection(t1)
{9, 7}
>>> s1.difference(t1)
{4, 8, 10}
Zadnji, vrlo važan, spremnik kojeg ćemo spomenuti je rječnik (engl.
dictionary ). Riječ je o preslikavanju key
-> value
, gdje je value
bilo koji
objekt, a key
može biti tipično broj ili string (premda su i druge stvari
dopustive kao key, npr tuplovi).
>>> d1 = {'a':1, 'c':3, 'b':2}
>>> d2 = {'ime': 'pero', 'prezime': 'peric', 'spol': 'M'}
>>> d1
{'b': 2, 'a': 1, 'c': 3}
Redosljed elemenata u rječniku nema značenja. Pristup pojedinim elementima rječnika je moguć “indeksiranjem” pomoću ključa:
>>> d1['c']
3
Na isti način je moguće dodavati nove elemente u rječnik:
>>> d2['telefon'] = '123456'
Iteriranje po rječniku ne daje elemente već samo ključeve
>>> [it for it in d1]
['b', 'c', 'a']
Dok za elemente treba koristiti metodu values
>>> [val for val in d1.values()]
[3, 2, 1]
A metoda items
i tehnika raspakiravanja tuplova daje oboje
>>> fmt = '{:>8s} {:s} {:10s}'
>>> print(fmt.format('ključ', '|', 'vrijednost'))
>>> print(fmt.format('-----', '+', '----------'))
>>> aux = [print(fmt.format(key, '|', val)) for key,val in d2.items()]
ključ | vrijednost
----- + ----------
spol | M
telefon | 123456
prezime | peric
ime | pero
Zadatak 6
Izračunajte prosjek godina ljudi iz slijedećeg rječnika (rezultat treba biti decimalni broj). Koristite funkciju mean
. Vaš kod neka ne sadrži ni jedan broj.
>>> ages = {'john' : 76, 'paul': 73, 'george': 73, 'ringo': 75}
Za kraj, i stringove možemo interpretirati kao liste znakova i tretirati ih kao takve
>>> s1 = 'Samobor'
>>> s1[-3:]
'bor'
3.1.4. NumPy polja¶
Obične liste se rabe za razne namjene, uključujući simboličke račune, ali za numeriku
su pogodnija tzv. NumPy polja. NumPy
(Numerical Python) je modul za python namjenjen baratanju s multidimenzionalnim
brojčanim poljima kakva se često sreću u svim područjima znanosti.
Riječ je o velikom modulu koji nije automatski prisutan u Pythonu,
već ga treba učitati pomoću naredbe import
>>> import numpy as np
(Gore smo već učitali taj modul. Višestruko učitavanje ne stvara probleme.)
Numpy polja su u računalu zapisana u neprekinutom slijedu memorijskih lokacija što omogućuje brži
pristup no zbog toga svi elementi polja moraju biti istog tipa
(integer
, float
, complex
...).
>>> a = np.array(range(1,11)); a # konverzija obične liste u NumPy polje
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
Vidimo da je NumPy polje ispisano na drugačiji način nego obična lista, ali
za razlikovanje tih dvaju objekata izgled ispisa nije pouzdani kriterij.
Neke operacije, poput print
, ispisuju NumPy polje da izgleda kao obična lista
>>> print(a)
[ 1 2 3 4 5 6 7 8 9 10]
Ako postoji dvojba, možemo ispitati tip objekta komandom type()
>>> type(a)
<class 'numpy.ndarray'>
Pri konstrukciji NumPy polja izbor tipa objekata je automatski, ali moguće ga je i odrediti opcionalnim argumentom dtype:
>>> b = np.array(range(1,11), dtype=np.float64); print(b)
[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
Tip objekata u NumPy polju je zapisan u dtype
atributu polja [1].
>>> a.dtype
dtype('int64')
>>> b.dtype
dtype('float64')
Numpy polja imaju dosta više metoda od običnih lista ...
>>> dir(a)[-66:]
['argpartition', 'argsort', 'astype', 'base', 'byteswap', 'choose', 'clip', 'compress', 'conj', 'conjugate', 'copy', 'ctypes', 'cumprod', 'cumsum', 'data', 'diagonal', 'dot', 'dtype', 'dump', 'dumps', 'fill', 'flags', 'flat', 'flatten', 'getfield', 'imag', 'item', 'itemset', 'itemsize', 'max', 'mean', 'min', 'nbytes', 'ndim', 'newbyteorder', 'nonzero', 'partition', 'prod', 'ptp', 'put', 'ravel', 'real', 'repeat', 'reshape', 'resize', 'round', 'searchsorted', 'setfield', 'setflags', 'shape', 'size', 'sort', 'squeeze', 'std', 'strides', 'sum', 'swapaxes', 'take', 'tobytes', 'tofile', 'tolist', 'tostring', 'trace', 'transpose', 'var', 'view']
... no zbog efikasnosti implementacije nema upravo onih metoda koje imaju
obične liste. To je zato jer je npr. umetanje elementa u listu vrlo “skupa”
operacija koja uključuje brisanje i pisanje svih elemenata koji u memoriji
dolaze nakon tog mjesta umetanja. Ako želimo raditi takve stvari upotreba NumPy
polja nam ionako neće donijeti nikakve prednosti i treba koristiti obične
liste. (Za konverziju NumPy liste u običnu koristimo funkciju list()
.)
Jedno od vrlo korisnih svojstava NumPy lista je da se većina matematičkih operacija prirodno distribuira po elementima liste (obične liste nemaju to svojstvo):
>>> xs = np.linspace(1,10,3); xs
array([ 1. , 5.5, 10. ])
>>> xs + 1
array([ 2. , 6.5, 11. ])
>>> sin(xs)
array([ 0.84147098, -0.70554033, -0.54402111])
NumPy liste mogu biti i višedimenzionalne, no i one su u memoriji računala zapisane u jednodimenzionalnom (1D) slijedu memorijskih adresa. Stoga je preoblikovanje elemenata 1D NumPy u 2D polje proizvoljnog oblika računalno “jeftina” operacija, koja može biti od koristi
>>> b=a.reshape(2,5); b
array([[ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10]])
>>> b.shape # "oblik" polja - broj redaka i stupaca
(2, 5)
Pristupanje pojedinom elementu višedimenzionalnog polja je moguće očitim indeksiranjem a[i][j]..., ali je efikasnije koristiti skraćeno indeksiranje: a[i, j, ...]:
>>> b[1,1] = 42.
>>> print(b)
[[ 1 2 3 4 5]
[ 6 42 8 9 10]]
Pažnja: radi efikasnosti, preoblikovanja i rezovi kroz NumPy polja ne kopiraju originalne
elemente već samo manipuliraju pokazivačima na ista mjesta. Tako se npr. ova netom
načinjena zamjena u listi b
odražava i u listi a
čijim preoblikovanjem
je lista b
nastala:
>>> print(a)
[ 1 2 3 4 5 6 42 8 9 10]
Obične liste nemaju takvo ponašanje:
>>> t5 = t1[2:4]; t5 # t1 je obična lista
[5, 7]
>>> t5[0] = 'novi'; t5
['novi', 7]
>>> t1
[1, 3, 5, 7, 9, 11, 13]
Inače, rezovi kroz dvodimenzionalne NumPy liste su elegantan način ekstrakcije redaka ili stupaca:
>>> b[:,1]
array([ 2, 42])
Ukoliko trebamo liste realnih (float) brojeva, možemo koristiti NumPy
funkciju arange()
(sintaksa je ista kao za range()
)
>>> np.arange(2., 9.9, 1.1)
array([ 2. , 3.1, 4.2, 5.3, 6.4, 7.5, 8.6, 9.7])
Funkcija np.arange()
je pogodna kad želimo specificirati korak liste.
Kad želimo
specificirati broj elemenata liste koristimo funkciju np.linspace()
.
>>> np.linspace(0, 10, 5)
array([ 0. , 2.5, 5. , 7.5, 10. ])
Za daljnje detalje o NumPy poljima, vidi NumPy Tutorial.
Zadatak 7
Za spajanje dvije jednako duge 1D liste u 2D listu (zapravo listu tuplova)
koristimo naredbu zip
>>> xs = [11, 12, 13, 14, 15] >>> ys = [-1, -2, -3, -4, -5] >>> points = list(zip(xs, ys)); points [(11, -1), (12, -2), (13, -3), (14, -4), (15, -5)]
Rastavite listu points
natrag na originalne dvije 1D liste i to
dvjema različitim metodama:
- Dva obuhvaćanja liste
points
(ova metoda nema veze s NumPy-em) - Dva reza kroz listu
points
, koju prvo pretvorite u NumPy polje pomoćunp.array
.
Footnotes
[1] | Atributi objekta su sve ono čemu se pristupa operatorom točkice . . Ranije spomenute metode su atributi koji se mogu pozivati kao funkcije. dtype je atribut koji je naprosto tip elementa polja. Za bolje razumijevanje svega ovog dobro je pročitati nešto o objektno-orijentiranom programiranju u Pythonu. |