Tokrat še bolj kot za ostale kratke zapiske velja, da predpostavljam, da bralec snov že pozna iz drugih jezikov. Torej, da mu objektno programiranje ni tuje in mu moramo povedati le, kako se te stvari zapiše v Pythonu.
Definirali bomo razred Turtle
, z metodami
forward(a)
, back(a)
, ki jo premakneta za
x
naprej oz. nazaj;turn(a)
, ki jo obrne za a
stopinj
levo;left()
, right()
, ki jo obrneta za 90
stopinj levo oz. desno.Iz tega razreda bomo izpeljali razred
TurtleProfessional
. Ta nam omogoča, da tiste stvari, ki so
v običajni želvi sicer zastonj, plačujemo. Poleg naštetih ima še
metodi:
set_cost(a)
, ki nastavi ceno za prehojeno enoto;get_total()
, ki pove, koliko smo želvi dolžni.Želva je v začetku na koordinatah (0, 0) in gleda desno. Privzeta cena plačljive želve je 1 na enoto premika.
from math import sin, cos, radians
class Turtle:
def __init__(self):
self.x = self.y = 0
self.heading = 0
def forward(self, a):
self.x += a * cos(self.heading)
self.y += a * sin(self.heading)
def backward(self, a):
self.forward(-a)
def turn(self, a):
self.heading += radians(a)
def left(self):
self.turn(90)
def right(self):
self.turn(-90)
class TurtleProfessional(Turtle):
def __init__(self):
super().__init__()
self.cost = 1
self.total = 0
def forward(self, a):
super().forward(a)
self.total += abs(a) * self.cost
def set_cost(self, a):
self.cost = a
def get_total(self):
return self.total
Preden povemo kaj več, se samo še zapeljimo z eno želvo.
= TurtleProfessional()
ana 10)
ana.forward(
ana.left()3)
ana.set_cost(5) ana.backward(
ana.get_total()
25
ana.x
10.0
ana.y
-5.0
Očitno in pričakovana. Rečemo class
, ime, dvopičje in
naštejemo funkcije/metode. Tako kot nikjer drugje tudi tu ni potrebno
deklarirati spremenljivk.
Če je razred izpeljan iz nekega drugega razreda, navedemo ta razred v oklepaju. Občutek imam, da imajo to jeziki v zadnjem času rajše, v Pythonu pa ima dvopičje itak še čisto drug pomen.
Poljem razreda (x
, y
, heading
...) se v Pythonu reče atributi (attribute). Potrebe po
slovenjenju ne vidim, saj je beseda v SSKJ brez klicajev. Poleg bi lahko
prevod "lastnost" zamenjevali s property, ki ima nekoliko
drugačen pomen.
O tem se kar takoj pogovorimo. En razred ima lahko tudi več prednikov; navedemo jih v oklepaju in ločimo z vejicami.
Večkratno dedovanje je običajno zelo slaba ideja, zato ga veliko jezikov sploh ne podpira. To je ena od tistih stvari, ki jih je celo Java naredila prav; katere so ostale tri, sem pozabil. Python to dopušča, vendar to izjemno rekdo vidimo; spomnim se samo enega razreda, za katerega vem, da je izpeljan iz dveh, vendar gre za wrapper okrog C-jevske knjižnice.
Zakaj Python potem to ima, če celo Java ve, da to ni OK in če to vejo tudi programerji?
Zelo redko (po mojih izkušnjah: nikoli) se ne zgodi, da bi en razred
hkrati spadal v dve vrsti razredov. Ali si Oseba
ali pa si
Kvadrat
in nihče, noben izpeljan razred, ne more biti
oboje.
Pač pa je večkratno dedovanje zelo uporabno in tudi neškodljivo, če ga ne dojemamo, kot da nekdo spada v dve vrsti reči, pač pa z dodatnim razredom dodamo ali obljubimo neko funkcionalnost.
En vzorec je mixin, s katerim razredu dodamo neko funkcionalnost. V bistvu gre za razred, ki definira neke dodatne metode, morda tudi kakšne z njimi povezane atribute, ne gre pa za nek funkcionalen, uporaben razred. Tipično (ne pa nujno) mixini tudi nimajo kakšne posebne hierarhije. V projektu, na katerem delam, imamo mixin za multi threading. Če nek razred "izpelješ" tudi iz tega mixina, dobi dodatne metode za delo s threadi. Imamo mixin za progress bar, ki grafičnim elementom doda progress bar. Mixin, ki kontrolira določene atribute...
Drugi vzorec, ki ga včasih uporabljamo v Pythonu, je označevanje
razredov. Razred izpeljemo iz nekega razreda, če želimo povedati, da zna
določene stvari. Razrede včasih izpeljemo tudi iz razredov v
typing
ali collections.abc
(abc = abstract
base class). Razred izpeljan iz collections.abs.Collection
,
recimo, o sebi trdi, da je čezenj možno z zanko for
, da zna
povedati svojo dolžino in ima operator in
. (Več v dokumentaciji.)
Oba vzorca spominjata na interface iz Jave in C#. Javin interface, če ga kot nekdo, ki nikoli ni veliko programiral v Javi, v zadnjih desetih letih pa sploh nič, bolj spominja na abstraktne razrede. Z njim obljubimo (oz. zahtevamo), da bo razred imel določene metode. Prevajalnik to preverja. Pythonova deklaracija abstraktnega podatkovnega tipa ima podoben učinek, vendar tega nihče ne preverja.
C# pa od različice 8.0 (iz leta 2019) omogoča, da v interface določimo privzete implementacije metod. To so v bistvu mixini.
Konstruktor je metoda z imenom __init__
. To je samo ena
izmed skoraj 50 posebnih metod, ki se začnejo in končajo z dvema
podčrtajema. (Ne zmotite se in ne napišite samo enega.)
Konstruktor seveda ni obvezen. Napišemo ga, če imamo v njem kaj povedati.
Konstruktor ima lahko tudi dodatne argumente. Te pač navedemo, ko
naredimo objekt. Če bi konstruktor razreda Turtle
definirali z
def __init__(self, name):
...
bi ga klicali z
= Turtle("Ana") ana
Destruktorjev ne pišemo skoraj nikoli. In v bistvu niso zares v paru
z __init__
, temveč z drugim konstruktorjem.
Python ima v bistvu dva konstruktorja. __init__
kot
argument prejme self
, saj je objekt že narejen. Naloga
__init__
-a je, da inicializira objekt, pripravi njegove
atribute. Metoda, ki v resnici naredi objekt, torej ustvari, vrne
self
pa je __new__
. Ta metoda kot argument
prejme razred in (praviloma) vrne objekt tega razreda.
Konstruktor, __del__
se pokliče, preden objekt odide iz
pomnilnika. Kdaj se bo to zgodilo, pa ne vemo. Takrat, ko ga bodo
smetarji naložili in odpeljali. Pri nas običajno okrog osme ure zjutraj;
v ponedeljek organske, v torek embalažo in tako naprej. Večina objektov
se razblini, ki nanje ne kaže nobeno ime več; če so udeleženi v kakšni
skupinski dejavnosti, ki obsega cikle, pa lahko tudi kdaj kasneje.
Ne le, da ne vemo, kdaj se bo __del__
poklical, temveč v
njem navadno tudi nimamo česa početi. Redki programerji v Pythonu so že
kdaj napisali konstruktor. Pretiravam, a ne veliko. :) Zase lahko povem,
da ga že precej let nisem napisal.
self
V jeziku (jezikih), iz katerega poznate objektno programiranje, ste
gotovo navajeni spremenljivke z imenom this
ali
self
. V Pythonu se ji reče self
. Python se od
jezika, ki ga poznate, najbrž razlikuje po tem, da je ta
self
vedno ekspliciten.
self
je vedno prvi argument metode. V C++, C#, Java,
Javascript ... se this
preprosto pojavi. Funkcija ga dobi
kot enega izmed argumentov, programer pa tega niti ne ve. Tu ve in ga
mora navesti. Razen, kadar ga ne, ker gre za statično ali razredno
metodo. A o tem bomo kasneje (če sploh).
Ob dostopanju do atributov in ob klicih metod moramo vedno
navajati self
. Ttako kot v Javascriptu (kjer je sicer
this
) in drugače kot v C++ in iz njega izpeljanih jezikih
(C++ sam pa je to pobral od Simule 67), kjer this
napišemo,
kadar se nam zdi.
Logika je v tem: ko smo šofirali Ano, smo pisali
ana.forward
. Vedno smo povedali kdo naj gre
naprej. Zakaj bi znotraj metod delali izjemo?
Praktičen razlog za to, je, da so v C++ in podobnih jezikih atributi
definirani. Tam se ve, da obstaja atribut x
, torej se bo
ime x
znotraj metod nanašalo na this->x
. V
Pythonu pa že v konstruktorju ne bi bilo jasno, ali hočemo z
x = 0
nastaviti lokalno spremenljivko ali atribut.
Poleg tega pa ima ta odločitev kup lepih posledic, ki pa jih lahko razumemo samo, če vemo, kako je Python narejen - to je, kako pravzaprav poišče objekt, na katerega se nanaša ime, kako v hierarhiji razredov poišče metodo ipd.
Tale self
boste malo pozabljali, potem pa se boste
navadili. Če sam zelo dolgo programiram samo v kakem drugem jeziku, se
včasih še sam spozabim.
super()
vrne objekt, ki je takšen kot self
,
vendar se vede, kot da bi bil objekt razreda, iz katerega je izpeljan ta
razred. Kaj v resnici vrne super()
in kako deluje, je
magija, v katero se, priznam, tudi sam nisem najglobje spustil. Vedno mi
je zadoščalo vedeti, da podedovani forward
pokličem s
super().forward
in podedovani konstruktor s
super().__init__
.
Python si je izmislil Nizozemec. Če ste se kdaj sprehajali po Amsterdamu, veste, kako (oz. po čem) tam diši. Skoraj kot v San Franciscu. Nizozemci ne skrivajo.
Python nima privatnih metod. Vse je javno. Pač pa se severni narodi bolj držijo družbenih norm.
class A:
def _ne_klici_me(self):
print("As mogu?")
= A()
a a._ne_klici_me()
As mogu?
class A:
def __res_me_ne_klici(self):
print("A ti je ratal?")
= A()
a a.__res_me_ne_klici()
--------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-7-4b5b4035e01e> in <module>
4
5 a = A()
----> 6 a.__res_me_ne_klici()
AttributeError: 'A' object has no attribute '__res_me_ne_klici'
a._A__res_me_ne_klici()
A ti je ratal?
Tako kot Python ne skriva metod, tudi ne skriva atributov. Zgoraj smo
videli, da lahko dostopamo do ana.x
in ana.y
.
Pisanje metod, s katerimi preberemo ali nastavimo vrednost atributa je
stvar okusa. Nekatere knjižnice so tozadevno zelo striktne, druge tega
nimajo.
Na tem mestu vas bo morda zanimalo, ali obstajajo propertyji, getterji in setterji. Da, ampak dajmo o tem kasneje.
Atribute smo dodajali v konstruktorju. Tako se spodobi.
Lahko pa jih dodajamo tudi v drugih metodah. Poljubna metoda lahko reče
self.nekaj = 42
. Tako se ne spodobi. V tem primeru
atribut obstaja samo, če pokličemo tisto metodo, sicer pa ne. To nekaj
časa deluje, potem pa nekaj spremenimo, malo drugače pokličemo metode,
in neha. pylint
vas bo zato opozoril, če v neki metodi
nastavljate atribut, ki ne dobi neke vrednosti že v konstruktorju.
Atribute lahko nastavljamo celo od zunaj.
= "Ana"
ana.name
ana.name
'Ana'
Tega ni možno početi, zato, ker so to dodali v jezik in bi bilo to posebej dobra praksa, temveč zato, ker tega niso vzeli iz jezika, preprečili. Točneje, ker bi bilo glede na to, kako je Python narejen, to pravzaprav zelo težko preprečiti.
Vse metode so virtualne.
Profesionalna želva je definirala svoj forward
. Ko
backward
(ki je definiran v razredu Turtle
pokliče forward
, se pokliče forward
, ki je v
razredu TurtleProfessional
.
Ugibam, da v prevajanih jezikih ni tako, ker lahko prevajalnik pri ne-virtualnih metodah optimizira kodo, poleg tega pa virtualne metode zahtevajo dodaten vpogled v tabelo, v kateri so zbrani kazalci na virtualne metode, medtem ko se za ne-virtualne že v času prevajanja (točneje: povezovanja, link) ve, kje v programu se bodo nahajale.
V jezikih, ki niso prevajani, ali pa vsaj v Pythonu te razlike ni.
V nekaterih jezikih je razred možno označiti kot final
-
iz takega razreda ni možno izpeljevati novih razredov. To je dobra
ideja.
Nekateri jeziki gredo še dlje: v Kotlinu je možno iz razreda
izpeljevati nove razrede samo, če so označeni kot open
. To
je še boljša ideja.
V Pythonu ni ničesar od tega.
Python ima od različice 3.8 dekorator @final
, s katerim
označimo razred, iz akterega naj bi se ne dalo izpeljevati novih
razredov, ali metodo, ki naj bi je ne bilo možno povoziti v izpeljanih
razredih. Kot vse te stvari pa je tudi to le stvar dogovora. Gre za
anotacijo tipa, tako kot lahko v Pythonu z i: int
prisežemo, da bo i
int
in v naslednji vrstici
iz gole hudobije v i
shranimo niz. I nikome ništa.
Pač pa na te dekoratorje prežijo razni linti in nas opozarjajo.
V projektu, na katerem delam, smo nekoč ugotovili, da bi radi zaprli vse razrede v določeni hierarhiji. Zato sem sprogramiral neko čarovnijo, s katero lahko naredimo tole:
class OurBaseClass:
# naš razred, ki vsebuje čarovnijo, ki preprečuje, da bi iz razredov,
# izpeljanih iz tega razreda, izpeljevali nove razrede
...
class A(OurBaseClass):
...
class B(A): # tu Python javi napako, da razred A ni odprt
...
class C(OurBaseClass, openclass=True):
...
class D(C): # to pa je dovoljeno, ker je C odprt
...
Je pa ta čarovnija prezapletena, da bi jo tule razlagal. Temelji na
uporabi meta
razredov. Na kratko, OurBaseClass
ni tip tipa tipa,
temveč tip nekega našega tipa.
Property je kot atribut, le da se v resnici vsakič znova izračuna.
class A:
def __init__(self, k):
self.k = k
@property
def dva_k(self):
return 2 * self.k
= A(5)
a a.dva_k
10
= 42
a.k a.dva_k
84
Kako naredimo property
, vidimo: okrasimo ga z
dekoratorjem. (Kot večina (vsi?) drugi dekoratorji je tudi ta takšen, da
bi ga, če še ne bi obstajal, lahko napisali sami. Samo malo več bi
morali vedeti o delovanju razredov.)
Lastnosti pogosto uporabljamo, kadar želimo nek podatek skriti in predvsem, kadar želimo nadzirati njegovo nastavljanje.
Sestavimo razred, ki bo vseboval atribut tabela
, katere
dolžina bo vedno tolikšna, kolikor je vrednost njegovega atributa
dim
.
class Tabela:
def __init__(self, dim):
self._dim = dim
self.tabela = [0] * dim
@property
def dim(self):
return self._dim
@dim.setter
def dim(self, new_dim):
if self._dim < new_dim:
self.tabela += [0] * (new_dim - self._dim)
else:
self.tabela = self.tabela[:new_dim]
self._dim = new_dim
= Tabela(4)
t t.tabela
[0, 0, 0, 0]
t.dim
4
= 10
t.dim t.dim
10
t.tabela
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Tako kot branje atributa dim
v resnici pokliče funkcijo
dim
, ki smo jo okrasili s property
,
nastavljanje tega atributa pokliče funkcijo, ki smo jo okrasili z
@dim.setter
(in jo moramo napisati za prvo funkcjo, saj
prvi dekorator ustvari drugega).
Tabela se je takrat, ko smo spreminili dim
, sama od sebe
podaljšala.
Lastnosti (property) lahko naredimo tudi drugače. Vsaj na dva načina. Mehanizacija, ki se skriva za tem, je kar zapletena; za vtis, si lahko ogledate dokumentacijo.
C++ in C# imata statične atribute (static member). Zakaj se jim reče statični, ne vem. V Pythonu so to atributi razredov.
class A:
= 12
x
def f(self):
return 42
= A()
a = A() b
Tako kot imata oba objekta metodo f
, imata tudi atribut
x
.
a.f()
42
a.x
12
Če objekt definira svoj x
, ima ta prednost pred
atributom razreda.
= 20
a.x
a.x
20
b.x
12
Skupni x
je pač A.x
.
A.x
12
= 13 A.x
b.x
13
Tudi tu še vedno velja vse, kar smo risali s puščicami.
class A:
= [1, 2, 3]
t
= A()
a = A()
b
4)
a.t.append( b.t
[1, 2, 3, 4]
is b.t is A.t a.t
True
= [10, 11, 12]
a.t b.t
[1, 2, 3, 4]
Tu je a
dobil svoj t
, b.t
pa
je še vedno, kar je bil prej.
Razredi imajo slovar, __dict__
, ki vsebuje metode (in
atribute razreda). Objekti imajo slovar __dict__
, ki
vsebuje atribute (in tudi metode, ampak o tem bomo govorili
posebej).
class A:
def __init__(self, ime):
self.ime = ime
self.t = 13
def f(self, x):
print(f"f razreda A, objekta {self.ime}, x={x}")
def g(self):
print(f"g razreda A, objekta {self.ime}")
class B(A):
def f(self, x):
print(f"f razreda B, objekta {self.ime}, x={x}")
= B("Ana")
ana = B("Berta") berta
ana
ima dva atributa, shranjena sta v slovarju
ana.__dict__
. berta
prav tako in sta pač v
b.__dict__
.
ana.__dict__
{'ime': 'Ana', 't': 13}
Ko torej hočemo dobiti vrednost ana.nekaj
, gre Python
iskat to vrednost v ana.__dict__
.
"prijatelj"] = "Benjamin"
ana.__dict__[
ana.prijatelj
'Benjamin'
Zapis ana.prijatelj
je samo okrajšava za
ana.__dict__["prijatelj"]
. No, skoraj. Ker imamo tu še
razrede.
Metode so, očitno, shranjene v slovarju razreda.
A.__dict__
mappingproxy({'__module__': '__main__',
'__init__': <function __main__.A.__init__(self, ime)>,
'f': <function __main__.A.f(self, x)>,
'g': <function __main__.A.g(self)>,
'__dict__': <attribute '__dict__' of 'A' objects>,
'__weakref__': <attribute '__weakref__' of 'A' objects>,
'__doc__': None})
B.__dict__
mappingproxy({'__module__': '__main__',
'f': <function __main__.B.f(self, x)>,
'__doc__': None})
Tole niso čisto pravi slovarji, vendar se vedejo ... precej kot da bi
bili. Ne moremo jim direktno spreminjati vrednosti (kot smo zgoraj
dodali "z"
v b
-jev slovar), ampak tega niti ne
potrebujemo.
Kaj se torej zgodi, ko napišemo b.k
ali
b.f
? Tole
"k"
,
"f"
) obstaja v slovarju samega objekta (b
-ja).
Če, potem ga vrne.Za vsak razred lahko izvemo, iz katerega razreda (oz. razredov) je izpeljan.
B.__bases__
(__main__.A,)
Še bolj zanimivo je, da lahko za vsak razred izvemo celo hierarhijo razredov nad njim. Da bo bolj zanimivo, dodajmo še en razred.
class C(B):
...
C.mro()
[__main__.C, __main__.B, __main__.A, object]
Vsi razredi imajo metodo mro
, ki vrne seznam razredov, v
katerih bo Python iskal metode (in atribute). Za razred C
so to razredi C
, B
, A
in na koncu
object
, iz katerih so izpeljani vsi razredi. (Metoda
mro
, recimo, je metoda razreda object
.)
Sestavimo primerek razreda C
.
= C(3.14) c
Ko torej napišemo c.f
se zgodi naslednje.
def poisci(objekt, ime):
if ime in objekt.__dict__:
print(f"Našel v slovarju objekta; {ime}={objekt.__dict__[ime]}")
return objekt.__dict__[ime]
for cls in type(objekt).mro():
if ime in cls.__dict__:
print(f"Našel med metodami razreda '{cls.__name__}'; {ime}={cls.__dict__[ime]}")
return cls.__dict__[ime]
print(f"Ime '{ime}'' ne obstaja.")
return None
"k") poisci(c,
Ime 'k'' ne obstaja.
"f") poisci(c,
Našel med metodami razreda 'B'; f=<function B.f at 0x7fc427951050>
<function __main__.B.f(self, x)>
"g") poisci(c,
Našel med metodami razreda 'A'; g=<function A.g at 0x7fc427946ef0>
<function __main__.A.g(self)>
"tega_ni") poisci(c,
Ime 'tega_ni'' ne obstaja.
Tule vidimo tudi, zakaj je tako dobro, da moramo vedno eksplicitno
navesti self
. Na ta način se bo ana.x
sproži
natančno isti mehanizem kot ob self.x
. Edino, v čemer se
Pythonove metode razlikujejo od običajnih funkcij, je, da ob klicu
ana.f
ana
postane prvi argument
f
-a.
Funkcija v Pythonu je preprosto objekt, ki se ga da poklicati.
def f(x):
return x * 2
f
<function __main__.f(x)>
Podobno so tudi metode samo objekti, ki se jih da poklicati. Le, da
imajo nek pripet self
.
ana.f
<bound method B.f of <__main__.B object at 0x7fc42792c890>>
ana.f
ni function
temveč
bound method
. Vendar se vede kot ... pač funkcija. Lahko jo
podamo funkcijam, ki pričakujejo funkcije, lahko jo priredimo
spremenljivki, lahko jo, seveda, tudi pokličemo.
= ana.f
anin_f = berta.f bertin_f
-5) anin_f(
f objekta razreda B, objekta Ana, x=-5
-5) bertin_f(
f objekta razreda B, objekta Berta, x=-5
Metoda bertin_f
, ki je seveda isto kot
berta.f
, je torej že vezana na Berto. Podobno velja tudi za
metode vdelanih objektov.
= [1, 2, 3] s
s.append
<function list.append(object, /)>
= s.append
t
4) t(
s
[1, 2, 3, 4]
Če je metoda lahko vezana, je očitno lahko tudi nevezana. To so metode razredov - ne objektov.
B.f
<function __main__.B.f(self, x)>
Nevezana metoda je dejansko funkcija. Pač funkcija, ki jo je potrebno poklicati z - v tem primeru - dvema argumentoma.
13) B.f(ana,
f objekta razreda B, objekta Ana, x=13
Če hočemo, jo lahko pokličemo tudi s kakim drugim objektom in Python se niti ne bo pritožil, razen če se bo moral.
1, 2, 3], 5) B.f([
--------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-52-f3adb971e019> in <module>
----> 1 B.f([1, 2, 3], 5)
<ipython-input-27-9de45a5a21d5> in f(self, x)
12 class B(A):
13 def f(self, x):
---> 14 print(f"f objekta razreda B, objekta {self.ime}, x={x}")
15
16 ana = B("Ana")
AttributeError: 'list' object has no attribute 'ime'
Tule ga je zmotilo, da self
(torej: seznam
[1, 2, 3]
nima atributa ime.
Lahko pa mu podtaknemo kakršnokoli reč, ki takšen atribut ima.
class F:
def __init__(self):
self.ime = "Anon"
= F()
d
13) B.f(d,
f objekta razreda B, objekta Anon, x=13
Objekt d
nima popolnoma nobene zveze z razredom
B
, vendar ga lahko podamo kot argument B
-jevi
metodi f
.
To je seveda grdo in tega ne počnemo.
Kje pa je to uporabno. Oh, je, seveda je. Recimo. Imejmo niz
= "1 2 3" s
Če ga hočemo razkosati, bomo navadno poklicali
s.split()
['1', '2', '3']
Je pa to isto kot
str.split(s)
['1', '2', '3']
Enkrat imamo pač vezano metodo
s.split
<function str.split(sep=None, maxsplit=-1)>
ki jo pokličemo brez argumentov (s.split()
), drugič
nevezano metodo
str.split
<method 'split' of 'str' objects>
ki jo pokličemo tako, da ji podamo niz, ki bo služil kot
self
, torej str.split(s)
.
Drugi klic je nenavaden in ga ne uporabljamo, pač pa nam lahko pride
prav, kadar želimo funkcijo str.split
podati kaki drugi
funkciji, na primer funkciji map
.
= ["1 2 3", "3 1", "3 5 8"]
ss list(map(str.split, ss))
[['1', '2', '3'], ['3', '1'], ['3', '5', '8']]
Zdaj, ko vemo, kako vse skupaj deluje, bomo lahko razumeli delovanje "monkey patchinga". V nekaterih jezikih je to običajna reč (primer je Ruby, na nek način pa tudi starejši JavaScript). V Pythonu to počnemo izjemoma, pogosto pa to delamo v testih.
class A:
def __init__(self, k):
self.k = k
def f(self, x):
return x + self.k
= A(3)
a
5) a.f(
8
Objekt a
(oziroma razred A
) očitno nima
metode g
. Lahko pa jo dobi.
def krat(self, x):
return x * self.k
= krat
A.g
5) a.g(
15
Kaj se je zgodilo? Nič posebnega: metoda g
se je
pojavila v A.__dict__
in se vede, kot da je tam že od
vedno.
A.__dict__
mappingproxy({'__module__': '__main__',
'__init__': <function __main__.A.__init__(self, k)>,
'f': <function __main__.A.f(self, x)>,
'__dict__': <attribute '__dict__' of 'A' objects>,
'__weakref__': <attribute '__weakref__' of 'A' objects>,
'__doc__': None,
'g': <function __main__.krat(self, x)>})
Seveda lahko tudi spremenimo katero od obstoječih metod.
def minus(self, x):
return x - self.k
= minus
A.f
5) a.f(
2
Kot rečeno, tega nikoli ne delamo v običajni kodi, pač pa to uporabljamo pri testiranju kode, kjer pri testiranju določenih metod zamenjamo nekatere druge metode z "mocki", ki sprejemajo oz. vračajo določene testne vrednosti, ali pa uporabljamo te mocke zato, da preverimo, ali je bila neka metoda res poklicana.
Več o tem pa, ko/če se bomo kdaj pogovarjali o testiranju.