Zapiski
Imena in objekti
Čeprav bomo danes pogosto uporabili besedo "objekt", se še ne bomo zares pogovarjali o objektnem programiranju. Besedo pa vseeno potrebujemo: z njo bomo mislili na "tisto, kar shranimo v pomnilniku", recimo kakšno število, seznam, niz, datoteko (malo kasneje pa celo kakšno funkcijo ali kakšen modul). Objekte bomo risali tako, da jih bomo zaprli v pravokotnik; za kakšne vrste objekt gre, pa bo razvidno iz tega, kar bo pisalo v tem pravokotniku.
Objekti so torej tisto, kar je. Do objekta (shranjene
številke, niza, seznama...), pridemo prek njegovega imena (x)
ali pa, posredno, prek, recimo indeksiranja (seznam_otrok[2]).
Na slikah, ki jih bomo risali, bo desna stran vsebovala objekte, leva imena.
Spomniti se moramo še kaj pomeni prirejanje. Prirejanje ima na desni strani nek izraz. Rezultat izračuna tega izraza je nek objekt: če je na desni strani izraz 1 + 1, bo rezultat izračuna objekt 2. Na levi strani prirejanja je ime: prirejanje priredi objekt, ki ga naračunamo iz desne strani, imenu, ki smo ga napisali na levi.
Kaj naredi naslednji program?
|
|
|
Naredi, kar kaže slika na desni. Prva vrstica sestavi objekt 1
in ga priredi imenu a. Nato naredi niz "Benjamin"
in ga priredi imenu b. V tretji vrstici se izraz na desni
izračuna tako, da Python sestavi objekt 2, nato sešteje ta objekt in objekt,
na katerega se nanaša ime a. Rezultat je objekt 3;
prireditveni stavek ga priredi imenu c.
Čeprav smo doslej govorili o spremenljivkah (in bomo še, navada je železna srajca), bomo vsaj danes rekli: Python nima spremenljivk, vsaj ne v takšnem pomenu besede, kot smo jih morda navajeni iz drugih jezikov. Python ima samo imena za objekte.
Čemu tega doslej nismo omenjali? Ker ni bilo potrebno. Nič takšnega se ni
dogajalo, da bi se morali ukvarjati s tem, kaj je zadaj. Zdaj, ko smo prišli
do funkcij, in sploh, ko bomo kmalu prišli do objektov, pa si moramo nekatere
stvari pojasniti. V preostanku zapiskov si bomo zato ogledali nekaj primer,
ki so presenetljivi. To seveda niso stvari, v katere bi se človek
vsakodnevno zaletel, temveč le izbrani primeri, ki dobro ilustrirajo, kako
delujejo spremenljivke imena.
Še nekaj povejmo: Python nikoli ne dela kopij objektov. Kadar bomo mislili, da imamo dve kopiji istega objekta, bomo imeli v resnici le dvakrat isti objekt - razen, kadar se posebej potrudimo, da bi dobili kopijo. Pa še takrat bo kopija navadno plitva. Posebej globoko v to temo nima smisla riniti; kdor hoče vedeti več o tem v Pythonu, naj pogleda dokumentacijo modula copy. Podobno velja tudi za druge jezike: da lahko v resnici skopiramo objekt, mora biti bodisi "serializable" (C#, Python, JavaScript...), podpirati kloniranje (Java) ali pa mora jezik omogočati dovolj introspekcije (Python, JavaScript...).
Primer 0
Preden zares začnemo, se moramo le še dogovoriti, kako bomo risali sezname. Recimo, da napišemo
|
|
|
1,
2 in
3, ter četrti objekt, seznam, ki vsebuje te tri objekte. Takšno risanje je nepraktično, zato bomo številke risali kar naravnost v sezname, kot kaže spodnja slika.
|
|
|
Primer 1
Sestavimo tri sezname, t, u in v.
|
|
|
Tri? V resnici imamo dva seznama. Prvi seznam sestavimo v prvi vrstici, tu
dvomov ni. Kaj naredi druga vrstica? Pogleda izraz desni strani enačaja: tam
piše t; izraz torej pravi "tisti seznam, ki smo ga poimenovali
t. Torej je u isto kot t. Seznam, ki
smo ga naredili v prvi vrstici, ima tako dve imeni, rečemo mu lahko
t ali u. V tretji vrstici pa sestavimo nov seznam,
v katerem so vsi elementi t-ja, od prvega do zadnjega; rezine
pač vedno sestavljajo nove sezname. Temu, novemu seznamu smo dali ime v.
Vsi trije seznami so enaki.
t in u se nanašata na en in
isti objekt, ime v pa na drugega. Ali se dve imeni nanašata na
isto, lahko preverimo z operatorjem is; operator
is je podoben operatorju ==, le da prvi preverja
istost, drugi pa samo enakost.
(Takoj opozorimo: is ni zamenjava za ==.
Nekateri študenti začnejo takoj, ko izvedo za is, navdušeno
pisati pogoje, kot je if ime is "Benjamin". To včasih celo
dela, ker Python izvaja neke zahrbtnosti; v resnici pa je ta, ki je to
napisal, najbrž hotel reči if ime == "Benjamin". Ne zamenjujte
is in ==!)
Zdaj seznamu u dodajmo še en element. Kaj dobimo?
|
|
|
u, smo spreminjali tudi
t, saj gre za eno in isto reč. Seznam, ki ga imenujemo
v, je ostal takšen, kot je bil.
Zdaj pa priredimo uju prazen seznam.
|
|
|
Tule je prvi kamen spotike za vse, ki niso bili dovolj pozorni, ko smo
povedali, kaj je prirejanje. Ko napišemo u = [],
ne spreminjamo
vrednosti spremenljivke u, temveč dodeljujemo nek
objekt imenu u. Razumite to in ste zmagali. Odtod naprej gre
vse po istem kopitu.
Ko imenu u priredimo nov objekt, to ne vpliva na objekt, ki ga
imenujemo t. Ostaja tak, kot je bil (saj ga nihče ni
spreminjal) in tudi ime t se še vedno nanaša nanj.
Primer 2
Naredimo prazen seznam, poimenujmo ga e; potem naredimo seznam,
ki vsebuje ta seznam.
|
|
|
Da sta e in ničti element t, t[0], res
eno in isto, se hitro prepričamo.
Kar bomo torej počeli z e, se bo zgodilo tudi s
t[0], saj se obe imeni nanašata na en in isti objekt.
|
|
|
t, da bomo videli, da je res.
t[0], se zgodi tudi z e.
|
|
|
e, da vidimo, da se je res spremenil.
e|
|
|
e se je nanašalo na določen objekt (seznam, ki vsebuje enko). Ta seznam smo pustili pri miru, torej
e-ja nismo spreminjali. Pač pa smo ustvarili nek nov objekt in ga priredili imenu
e. Ker je ostal objekt, na katerega se je prej nanašalo ime, nedotaknjen, se tudi
t ni spremenil:
t še vedno vsebuje isti objekt in ta objekt je še vedno seznam, ki vsebuje enico, kot kaže slika in potrjuje spodnji poskus:
Pač pa se je e spremenil v tem smislu, da se ime
e ne nanaša več na isti objekt kot prej.
Primer 3
Sezname lahko, vemo, množimo s števili. Kar dobimo, je seznam, ki vsebuje večkrat ponovljen prvi seznam.
|
|
|
t ne vsebuje treh praznih seznamov, temveč trikrat vsebuje isti prazen seznam, znan tudi pod imenom
e.
Če je kdo predpostavil, da bodo to trije ne-isti seznami (torej enaki, saj
so vsi prazni, vendar ne tudi isti), je zmotno mislil, da bo
t = [e] * 3 naredil tri kopije eja. Ne, v
t da trikrat isti e.
Ker gre za trikrat (štirikrat, če štejemo še e) isti seznam, se
z enim spreminjajo vsi trije.
|
|
|
Isto (da, enako bi bila tu prešibka beseda) bi se zgodilo tudi, če
bi namesto k seznamu e dodali enico k seznamu t[0],
t[1] ali t[2].
Kakšen je zdaj t, vemo. Le prepričajmo se:
In zdaj vas vprašam: se je t spremenil ali ne? Tisti, ki na to
vprašanje odgovorijo z "da" ali z "ne", ne razumejo. Pravilen odgovor je, da
vprašanje ni povsem jasno. V bistvu se t ni spremenil, saj ga
tudi ni nihče spreminjal: t še vedno vsebuje natančno isto reč
kot prej, trikrat en in isti objekt, seznam, ki ga poznamo tudi pod imenom
e. Res pa se je spremenil ta seznam. Torej je t
ostal enak, spremenilo se je le tisto, kar t vsebuje.
To je tako, kot da bi imel v roki pladenj. Če na ta paldenj nekaj dam ali z njega kaj vzamem, imam v roki še vedno isti pladenj, le različne reči so na njem. Ali po tem držim iste stvari ali ne, je stvar besed.
Primer 4
Naredimo nekaj podobnega kot prej: prazen seznam in nov seznam, v katerem bo trikrat ta, prazni seznam. V ta slednji seznam dodajmo še en prazen seznam.
|
|
|
t sicer vsebuje štiri prazne sezname, vendar so prvi trije isti, zadnji pa le enak.
Če spremenimo e (se pravi, če vanj dodamo enico), se spremenijo
prvi trije elementi tja, saj gre za isti seznam, četrti (t[3])
pa ostane, kakršen je bil, namreč prazen.
|
|
|
Primer 5
More seznam vsebovati sam sebe? Tega ni težko preskusiti.
|
|
|
Koliko elementov ima zdaj seznam t? Neskončno? Ne, nikakor,
noben seznam ne more imeti neskončno elementov, to bi ne šlo v pomnilnik.
Samo štiri ima, namreč 1, 2, 3 in še tisti seznam, ki ga poznamo tudi pod
imenom t.
t sam:
t sam: če izpišemo
t ali t[3], je to eno in isto.
Pa dodajmo v t še en element.
|
|
|
t še en element več, prav tako ima
t[3] en element več, saj sta t in
t[3] še vedno ena in ista reč.
Primer 6
Definirajmo funkcijo, ki sprejme dva argumenta in ju malo spremeni.
Vzemimo zdaj eno številko in en seznam.
|
|
|
Pokličimo funkcijo, kot argumente ji dajmo x in y.
|
|
|
a in
b: ker smo v oklepaju v definiciji funkcije navedli dve imeni, bo funkcija pričakovala dva argumenta, dva objekta. Ob klicu se imenoma argumentov,
a in
b, priredita objekta, poslana kot argumenta. Zgodi se isto, kot če bi rekli
a = x in b = y. Če bi namesto f(x,
y) funkcijo poklicali z f(sin(x)*2, ",
".join(imena), bi se zgodilo isto, kot če bi na začetku funkcije rekli
a = sin(x)*2 in b = ", ".join(imena).
Imeni a in b se nanašata na ista objekta kot imeni
x in y. Dril od tod naprej nam je znan in nič nas
ne sme več presenetiti. Funkcija najprej reče
|
|
|
a? Takšnemu govorjenju se bomo danes, smo rekli, izogibali. Objekta, ki smo ga poprej imenovali
a (in ga imenujemo tudi
x), nismo spremenili, ostal je tak, kot je bil (in
x z njim). Pač pa smo naredili objekt
2 in ga priredili imenu a.
Sledi spreminjanje bja.
|
|
|
b, točneje, objekt, ki ga imenujemo
b. Ker ima isti objekt tudi ime
y, se spreminja tudi objekt, ki ga imenujemo
y. Če tako po vrnitvi iz funkcije izpišemo x in y, izvemo
Posebnost: inkrementalni operatorji
Operatorji, kot so +=, -= in *=
zahtevajo posebnost. Nekateri objekti so, kot vemo, nespremenljivi - takšne
so terke, pa tudi nizi in števila. Če rečemo a += 1, se ne
spremeni objekt, ki smo ga prej imenovali a, temveč se k njemu
prišteje 1 in rezultat (na novo) priredi imenu a.
Z drugimi besedami, a += 1 je isto kot a = a + 1.
Če je objekt slučajno spremenljiv - edini, ki ga poznamo in podpira
+=, je seznam - pa += ne sestavlja novega objekta,
temveč spreminja obstoječega.
|
|
|
Operator += enkrat spreminja objekt, drugič naredi novega.
Ta izjema je neugledna. Sramotna. Ampak praktična. Pravilo, po katerem se
ravna, je preprosto, operator += pa tako ali tako podpira malo
tipov, torej se bomo hitro navadili, kako se obnaša pri katerem.