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 0x7fadccbd6d10>
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 0x7fadccc14b80>
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 objektuxpokličex.__add__(y), podobno je z ostalimi aritmetičnimi operacijami__sub__,__mul__, …ko napišemo
x == y, se na objektuxpokličex.__eq__(y),ko napišemo
x in y, se na objektuypokličey.__contains__(x),ko napišemo
x[i], se na objektuxpoklič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 0x7fadc8725230>
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 0x7fadc8725770>
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 objektaobjdobiš iteratoritpokliči
next(it), da dobiš naslednjo vrednostvrednost shrani v spremenljivko
xizvedi ukaze
...ponavljaj korake od 2 do 4, dokler korak 2 ne sproži izjeme
StopIterationko 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