Razredi#
V Pythonu sorodne vrednosti združujemo v razredih. Posameznim pripadnikom razredov pravimo objekti. Vsi objekti istega razreda uporabljajo isto definicijo metod, vsak izmed njih pa ima svoje vrednosti, ki jim pravimo atributi. Na primer, seznami pripadajo razredu list
. Vsak seznam ima svoje elemente, vsi pa podpirajo iste metode kot so append
, pop
ali sort
, ki se na vsakem seznamu obnašajo na isti način. Nizi pripadajo razredu str
. Vsak niz ima svoje znake, vsi pa imajo skupno definicije metod kot so split
, upper
in podobno.
Definicije lastnih razredov#
Če želimo, lahko definiramo tudi svoj razred. To naredimo z ukazom class
, ki mu sledi ime razreda (pisano z veliko začetnico) ter zamaknjene definicije metod. Na primer, definirajmo si razred, ki bo predstavljal aritmetična zaporedja \(a, a + d, a + 2d, a + 3d, \ldots\). Za začetek definirajmo nobene metode, zato v definicijo napišimo le pass
.
class AritmeticnoZaporedje:
pass
Nove objekte danega razreda dobimo tako, da napišemo ime razreda ter oklepaje:
zap = AritmeticnoZaporedje()
zap
<__main__.AritmeticnoZaporedje at 0x7fb4a2711090>
Dobili smo prazen objekt razreda AritmeticnoZaporedje
, vendar z njim težko naredimo kaj pametnega. Do atributov objektov dostopamo prek ime_objekta.ime_atributa
, vendar trenutno zaporedjem nismo nastavili še nobenega atributa:
zap.zacetni_clen
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[3], line 1
----> 1 zap.zacetni_clen
AttributeError: 'AritmeticnoZaporedje' object has no attribute 'zacetni_clen'
Na podoben način lahko atribute tudi nastavljamo:
zap.zacetni_clen = 2
zap.razlika = 5
zap.zacetni_clen + 8 * zap.razlika
42
Definirajmo si tri metode: prva bo nastavila atributa, v katerih bosta shranjena začetni člen in razlika, drugi dve pa bosta izračunala člen na danem mestu ter vsoto do vključno tega člena.
class AritmeticnoZaporedje:
def nastavi_atribute(self, zacetni_clen, razlika):
self.zacetni_clen = zacetni_clen
self.razlika = razlika
def clen(self, i):
return self.zacetni_clen + i * self.razlika
def vsota(self, i):
return (i + 1) * self.zacetni_clen + i * (i + 1) // 2 * self.razlika
Metode definiramo na enak način kot funkcije, le da na prvem mestu sprejmejo še dodatni argument, ki ga ponavadi imenujemo self
, prek njega pa dostopamo do objekta, na katerem smo poklicali metodo.
zap = AritmeticnoZaporedje()
zap.nastavi_atribute(2, 5)
zap.clen(0)
2
zap.vsota(5)
87
Ker objekt brez nastavljenih atributov ni preveč koristen, si jih bomo želeli nastaviti na začetku, ko objekt ustvarimo. To storimo tako, da definiramo posebno inicializacijsko metodo z imenom __init__
, ki jo bo Python poklical takoj za tem, ko ustvari nov prazen objekt. Metodi __init__
lahko podamo tudi argumente, ki jih podamo ob konstrukciji objekta. Zgornjo definicijo razreda bi tako bolje napisali kot:
class AritmeticnoZaporedje:
def __init__(self, zacetni_clen, razlika):
self.zacetni_clen = zacetni_clen
self.razlika = razlika
def clen(self, i):
return self.zacetni_clen + i * self.razlika
def vsota(self, i):
return (i + 1) * self.zacetni_clen + i * (i + 1) // 2 * self.razlika
zap = AritmeticnoZaporedje(2, 5)
zap.clen(8)
42
Seveda lahko ustvarimo tudi več objektov:
liha_stevila = AritmeticnoZaporedje(1, 2)
liha_stevila.vsota(99)
10000
Posebne metode#
Poleg metode __init__
smo dogovorjeni še za nekaj posebnih metod, ki se pokličejo ob danih trenutkih. Na primer, trenuten izpis v konzoli je precej nekoristen.
zap
<__main__.AritmeticnoZaporedje at 0x7fb4a27112d0>
Izpis lahko popravimo z definicijo metode __repr__
, ki vrne predstavitev objekta z nizem, Python pa jo pokliče, ko želi izpisati rezultat na konzolo. Podobna ji je metoda __str__
, ki se pokliče takrat, ko objekt izpišemo. Namen prve je, da vrne izpis, koristen za nadaljnje delo v Pythonu, namen druge pa človeku prijazen izpis.
class AritmeticnoZaporedje:
def __init__(self, zacetni_clen, razlika):
self.zacetni_clen = zacetni_clen
self.razlika = razlika
def __repr__(self):
return f"AritmeticnoZaporedje({self.zacetni_clen}, {self.razlika})"
def __str__(self):
return f"{self.clen(0)}, {self.clen(1)}, {self.clen(2)}, ..."
def clen(self, i):
return self.zacetni_clen + i * self.razlika
def vsota(self, i):
return (i + 1) * self.zacetni_clen + i * (i + 1) // 2 * self.razlika
zap = AritmeticnoZaporedje(2, 5)
zap
AritmeticnoZaporedje(2, 5)
print(zap)
2, 7, 12, ...
Takih metod je še precej:
ko napišemo
x + y
, se na objektux
pokličex.__add__(y)
, podobno je z ostalimi aritmetičnimi operacijami__sub__
,__mul__
, …ko napišemo
x == y
, se na objektux
pokličex.__eq__(y)
,ko napišemo
x in y
, se na objektuy
pokličey.__contains__(x)
,ko napišemo
x[i]
, se na objektux
pokliče bx.__getitem__(i)
in tako naprej.
Seznam vseh posebnih metod najdete v uradni dokumentaciji.
class AritmeticnoZaporedje:
def __init__(self, zacetni_clen, razlika):
self.zacetni_clen = zacetni_clen
self.razlika = razlika
def __repr__(self):
return f"AritmeticnoZaporedje({self.zacetni_clen}, {self.razlika})"
def __str__(self):
return f"{self.clen(0)}, {self.clen(1)}, {self.clen(2)}, ..."
def __getitem__(self, i):
return self.zacetni_clen + i * self.razlika
def __contains__(self, x):
return (x - self.zacetni_clen) % self.razlika == 0
def __add__(self, other):
zacetni_clen_vsote = self.zacetni_clen + other.zacetni_clen
razlika_vsote = self.razlika + other.razlika
return AritmeticnoZaporedje(zacetni_clen_vsote, razlika_vsote)
def __eq__(self, other):
return self.zacetni_clen == other.zacetni_clen and self.razlka == other.razlika
def vsota(self, i):
return (i + 1) * self.zacetni_clen + i * (i + 1) // 2 * self.razlika
zap1 = AritmeticnoZaporedje(2, 5)
zap2 = AritmeticnoZaporedje(7, 3)
zap1 + zap2
AritmeticnoZaporedje(9, 8)
zap1[8]
42
42 in zap2
False
zap1 + zap2 == AritmeticnoZaporedje(1, 8)
False
Iteracija#
Iteratorji#
V Pythonu se dostikrat sprehajamo čez znake v nizu, elemente seznama, števila v range
, … Vse dosežemo prek tako imenovanih iteratorjev. To so objekti, ki imajo posebno vgrajeno metodo __next__
, ki ob vsakem klicu vrne naslednjo vrednost. Ko vrednosti zmanjka, metoda to sporoči tako, da sproži izjemo StopIteration
. Za primer napišimo iterator, ki zaporedoma vrača znake niza:
class IteratorCezNiz:
def __init__(self, niz):
self.niz = niz
self.indeks = 0
def __next__(self):
if self.indeks < len(self.niz):
znak = self.niz[self.indeks]
self.indeks += 1
return znak
else:
raise StopIteration
it = IteratorCezNiz('abc')
Metodo __next__
bi lahko poklicali direktno, kot it.__next__()
, vendar jo je lepše poklicati prek vgrajene funkcije next
(ki ne naredi drugega, kot da na svojem argumentu pokliče __next__
).
next(it)
'a'
next(it)
'b'
next(it)
'c'
next(it)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Cell In[26], line 1
----> 1 next(it)
Cell In[21], line 12, in IteratorCezNiz.__next__(self)
10 return znak
11 else:
---> 12 raise StopIteration
StopIteration:
Če želimo, se lahko iteratorji tudi nikoli ne končajo, s čimer lahko predstavimo neskončne sezname, na primer vsa Fibonaccijeva števila.
class IteratorFibonaccijevih:
def __init__(self):
self.a, self.b = 0, 1
def __next__(self):
prejsnji_a = self.a
self.a, self.b = self.b, self.a + self.b
return prejsnji_a
fib = IteratorFibonaccijevih()
[next(fib) for _ in range(10)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Generatorji#
V Pythonu obstaja zelo enostaven način, na katerega pišemo iteratorje, in to so generatorji. Recimo, da bi želeli le izpisati vse znake v nizu. Tedaj bi lahko napisali funkcijo:
def znaki_niza(niz):
i = 0
while i < len(niz):
print(niz[i])
i += 1
Pri funkciji nalašč nismo uporabili zanke for
, saj bi bilo to goljufanje.
znaki_niza("abc")
a
b
c
To je res izpisalo vse znake, vendar si z njimi ne moremo pomagati, saj so šli naravnost na konzolo. Če bi jih želeli vrniti, bi lahko napisali sledečo funkcijo, vendar bi ta ob prvem return
prenehala z izvajanjem:
def znaki_niza(niz):
i = 0
while i < len(niz):
return niz[i]
i += 1
znaki_niza("abc")
'a'
Tu v igro pridejo generatorji. Dobimo jih tako, da namesto return
napišemo yield
. Ko bo izvajanje klica prišlo do ukaza yield
, se ne bo končalo, temveč le zaustavilo, ter se od tam nadaljevalo ob naslednjem klicu next
.
def znaki_niza(niz):
i = 0
while i < len(niz):
yield niz[i]
i += 1
gen = znaki_niza("abc")
gen
<generator object znaki_niza at 0x7fb4a04f7530>
next(gen)
'a'
next(gen)
'b'
next(gen)
'c'
next(gen)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Cell In[39], line 1
----> 1 next(gen)
StopIteration:
Tudi generator Fibonaccijevih števil lahko dobimo na podoben način:
def fibonaccijeva_stevila(a=0, b=1):
while True:
yield a
a, b = b, a + b
f = fibonaccijeva_stevila()
[next(f) for _ in range(10)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
f = fibonaccijeva_stevila(0, 10)
[next(f) for _ in range(10)]
[0, 10, 10, 20, 30, 50, 80, 130, 210, 340]
Pri poimenovanju je treba biti natančen: fibonaccijeva_stevila
niso generator, temveč funkcija, ki ob vsakem klicu vrne generator, ki je posebna vrsta iteratorja.
fibonaccijeva_stevila
<function __main__.fibonaccijeva_stevila(a=0, b=1)>
fibonaccijeva_stevila()
<generator object fibonaccijeva_stevila at 0x7fb4a04f7b50>
Iterabilni objekti#
Objekti, po katerih se sprehajamo z zanko for
, niso iteratorji. Na primer, nizi so sestavljeni natanko iz svojih znakov, zato si ne morejo “zapomniti”, katere znake so že vrnili in katerih ne. Ampak, kot smo videli, iz njih lahko naredimo smiselne iteratorje. Takim objektom pravimo iterabilni. Natančneje zanka for x in obj: ...
deluje na sledeči način:
pokliči
iter(obj)
, da iz iterabilnega objektaobj
dobiš iteratorit
pokliči
next(it)
, da dobiš naslednjo vrednostvrednost shrani v spremenljivko
x
izvedi ukaze
...
ponavljaj korake od 2 do 4, dokler korak 2 ne sproži izjeme
StopIteration
ko dobiš izjemo, končaj
Če želimo na objektih svojega razreda podpreti zanko for
, moramo definirati metodo __iter__
, ki bo vrnila iskani iterator. Na primer, iterator po aritmetičnih zaporedjih bi lahko napisali kot:
class AritmeticnoZaporedje:
def __init__(self, zacetni_clen, razlika):
self.zacetni_clen = zacetni_clen
self.razlika = razlika
def __iter__(self):
x = self.zacetni_clen
while True:
yield x
x += self.razlika
zap = AritmeticnoZaporedje(2, 5)
for x in zap:
print(x)
if x > 100:
break
2
7
12
17
22
27
32
37
42
47
52
57
62
67
72
77
82
87
92
97
102