Zapiski
Spremenljivke so doslej kar ... bile. Uporabljali smo jih, postavljali smo njihove vrednosti, jih brali in spreminjali, kako reč deluje, pa nas ni vznemirjalo. Čas je, da se naučimo nekaj o tem. Ne veliko, ne vsega, le toliko, da si razširimo obzorja.
Tale snov je zanimiva, ker je za te, ki so znali programirati že od prej, težja od tistih, ki se učijo na novo. Slednji bodo vzeli stvari na znanje; tokrat so tisti, ki se bodo morali potruditi, da se jim bo odprlo, ravno tisti, ki znajo največ. Tako kot so doslej začetniki mislili, da je nekaj zapleteno, znalci pa so vedeli, da je enostavno, se bo tole zdelo preprosto (vsaj kolikor toliko) začetnikom, znalci pa bodo mislili, da je zapleteno - ker ne znajo razmišljati preprosto.
Za tiste, ki že znajo programirati
Začetnikov se tole ne tiče, zato naj cel razdelek brez škode preskočijo.
Tiste, ki so se že kdaj učili programirati in so uporabljali Cju podobne
jezike, pa bo reč zmedla. V zbirniku je bila spremenljivka le ime za
pomnilniško lokacijo. Programer se je odločil, da bo na neko mesto v
pomnilniku zapisal ta in ta podatek. Da mu ni bilo treba stalno pisati
naslova (torej neke številke) - sploh potem, ko jih je bilo že veliko in še
bolj potem, ko si naslovov ni več izmišljal sam, temveč jih je dodeljeval
prevajalnik - je nadel pomnilniški lokaciji ime, npr. a
, ali
b
ali imena
. Ko je na osnovi zbirnika nastal C, je
uporabljal isto logiko: spremenljivka je samo ime za pomnilniško lokacijo.
Ko definiramo spremenljivko (npr. ko napišemo int a;
)
prevajalnik rezervira prostorček v pomnilniku in kadarkoli bomo rekli a
,
se bo to nanašalo na ta prostorček. Bolj ali manj enako delujejo tudi
jeziki, izpeljani iz Cja, kot so C++, Java, C# in, odvisno od tega, koliko
raztegnemo "bolj ali manj enako", še večina drugih jezikov, ki ste jih morda
srečali.
Princip "ime je sinonim za naslov v pomnilniku" je zastarel in v jezikih, v katerih nam ne bi bilo več potrebno razmišljati o pomnilniku, povzroča same težave in nepreglednost. Spremenljivka je lahko sinonim za naslov (tako kot prej), lahko je sinonim za drugo spremenljivko (referenca), lahko je kazalec na naslov, če jezik pozna kazalce... Obenem se načelo ne ujame dobro z objekti. Splošnonamenski jeziki so danes praviloma objektni, vendar pogosto le napol. Nekatere stvari so objekt, nekatere pa ne. "Neobjekti" niso le, recimo, primitivi v Javi in C#, tudi funkcija v teh jezikih ni objekt.
Da bi bila funkcija objekt?! Da, seveda. Tudi tipi so objekti, metode so
lahko objekti, moduli so objekti. V Pythonu in drugih spodobnih jezikih je
vse objekt. Tisti, ki si misli, "jaz že vem, kaj je objekt in funkcija pač
ni objekt", je samo zakoreninjen v neki pol-objektni paradigmi, ki naj jo
čimprej pozabi. Čim rečemo, da je vse objekt, pa moramo spremeniti tudi
pogled na to, čemu bomo rekli spremenljivka. Tudi funkcija bo neke vrste
spremenljivka; tako kot je po a = 1
a
spremenljivka tipa int
, bo po def f(x): return 42
ime f
predstavljalo spremenljivko tipa function
.
Vse to je seveda nezdružljivo z dobrega pol stoletja starim pogledom, po katerem so spremenljivke samo pomnilniški naslovi. Če se vam bo torej zdelo to, kar boste brali v nadaljevanju, čudno in zmedeno ... ste v resnici čudni in zmedeni le vi. ;) Jeziki, ki niso le "poflikan C", so navadno veliko jasnejši; nekomu, ki pričakuje enako zmedo, kot vlada v Cju, pa se zna biti težko znebiti vsega balasta, ki ga nosi v glavi, in videti stvari tako preproste, kot so v resnici.
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. Da pridemo do objekta (shranjene
številke, niza, seznama...), pa potrebujemo njegovo ime. Vsak objekt ima eno
ali več imen. Ime je lahko direktno, kot na primer a
ali
seznam_otrok
,
danes pa bomo malo onečedili terminologijo z "nedirektnimi imeni" in rekli,
da je tudi seznam_otrok[2]
ime nekega objekta (drugega objekta
v seznamu otork pač).
Na slikah, ki jih bomo risali, bo desna stran vsebovala objekte, leva imena.
Spomniti se moramo še - ali pa celo malo drugače povedati - kaj pomeni prireditveni stavek. 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, spet bolj za ne-začetnike: 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
.
Vi 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.
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 u
ju prazen seznam.
|
Tule je prvi kamen spotike za vse, ki mislijo, da so t
,
u
in v
spremenljivke v pomenu, kot so ga morda
vajeni od drugod. Niso, vsaj danes se tega
zavedamo. To so le imena, ki jih dajemo objektom (oziroma obratno, imenom
dodeljujemo objekte). Ko reč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 in dvojko, 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 e
ja. 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 t
ja, 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 b
ja.
|
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.
Spremenljivke so lokalne
Spomnimo se popolnih števil. Napišimo funkcijo, ki pove, ali je dano število
popolno in drugo, ki vrne seznam vseh popolnih števil do 1 do 1000. Da bo
tisto, kar bomo želeli povedati, bolj nazorno, ju bomo sprogramirali z
(nerodnejšo) zanko while
namesto for
.
Spotaknili se bomo ob tole: funkcija vsa_popolna
uporablja
spremenljivko v
, da vanjo shrani seznam popolnih števil in
i
, da z njo šteje od 1 do 1000. Kaj se zgodi, ko vsa_popolna
pokliče funkcijo je_popolno
? Ta ji vse pokvari! (Ji res?)
Vrednost v
postavi na 0 in potem vanjo nekaj sešteva,
i
pa postavi na 1 in z njim šteje do n! (Ga res?) Po koncu te
funkcije, ob vrnitvi v vsa_popolna
, v
ni več
seznam, temveč število! (Je res?)
Samo brez panike. Tako ne bi šlo, zato Python ni narejen tako. Vsaka funkcija
ima svoje spremenljivke, točneje, svoja imena; v
iz funkcije
vsa_popolna
ni isto ime kot v
iz funkcije je_popolno
.
Imena se med seboj "ne tepejo". Takšnim spremenljivkam pravimo lokalne
spremenljivke.
Če so nekatere spremenljivke lokalne, najbrž obstajajo tudi takšne, ki to niso? "Globalne spremenljivke" torej? Da. Poglejmo drug, čistejši primer.
Koliko x-ov imamo? "Glavni program" nastavi x
na 42. Ta
x
, ki ni definiran znotraj nobene funkcije, je globalna
spremenljivka. Nato pokličemo funkcijo f
; ta poskuša
izpisati x
in ker ga ni med lokalnimi spremenljivkami, ga išče
(in najde) med globalnimi. Nato pokličemo funkcijo g
; ta
nastavi x
na 13. Vsa imena, ki jim prirejamo objekte znotraj
funkcij, so lokalna imena, torej ima funkcija g
svoj, lokalni
x
, ki nima ničesar z globalnim x
(razen tega, da
je ime slučajno enako, tako kot smo imeli zgoraj slučajno v dveh funkcijah
spremenljivki z enakim imenom v
). Funkcija
g
seveda izpiše 13. Program se konča z izpisom globalnega
x
, ki je ostal 42.
Lahko funkcija spreminja globalne spremenljivke? Pazimo, pogovarjajmo se raje
o objektih in imenih. Objekti ne morejo biti globalni ali lokalni, globalna
ali lokalna so le imena. V spodnjem primeru funkcija spremeni
objekt, na katerega se nanaša globalno ime x
.
x
:
Res res res ne gre? Kaj pa, če funkcija res MORA spreminjati "globalno spremenljivko" (se pravi, določiti objekt globalnemu imenu)? Če pridete v situacijo, ko bi si to želeli, premislite, ali ste dobro zastavili svoj program. Navadno tega v resnici nočete. Primeri, ko bi bilo to res potrebno, so tako redki, da vam raje sploh ne povem, kako se to naredi, saj bi v tem primeru to stalno počeli in to ni lepo.
Zakaj pa ne? Kaj ni lepo? Če se da... zakaj ne bi smeli?
Lepo je, če vsaka funkcija s "svetom" komunicira le prek argumentov in rezultata. Vse podatke, ki jih potrebuje, dobi kot argumente, in vse, kar vrne, vrne kot rezultat. Na ta način je obvladljiva: točno je določeno, kaj dela, lahko jo testiramo in ko je enkrat testirana, vemo, da dela. Spreminjanje globalnih reči je "stranski učinek" funkcije in stranskih učinkov nočemo, ker zapletejo program.
"Lestvica nezaželenosti" je takšna. Prvo: funkcije včasih berejo vrednosti
globalnih spremenljivk. To ni posebej lepo, je pa praktično. Predvsem v kakem
programu, dolgem borih nekaj sto vrstic, se zaradi tega ne bomo čisto nič vznemirjali. Drugo:
funkcije včasih spreminjajo objekte, do katerih so prišli prek globalnih
imen, tako kot zgoraj v x.append(1)
. To ni preveč lepo, vendar
smo tudi s tem pripravljeni živeti, če res ne gre drugače. Včasih pa bi
želeli v funkciji prirejati globalnim imenom, kot smo poskušali (neuspešno,
ker nisem povedal, kako se to naredi) na koncu. Kadar vas skušnjavec
nagovarja k temu, mu recite NE.
Kje so shranjena imena
V Pythonu so spremenljivke shranjene kar v slovarjih, katerih ključi so
imena, vrednosti pa objekti, na katere se imena nanašajo. Prva slika iz teh
zapiskov je shranjena kot slovar {"a": 1, "b": "Benjamin", "c":
3}
. Vsaka funkcija ima svoj slovar z lokalnimi spremenljivkami.
Poleg tega ima vsaka funkcija dostop tudi do globalnega
slovarja, ki, očitno, vsebuje globalne spremenljivke. Tudi glavni program
ima svoj slovar lokalnih spremenljivk, ki je v tem primeru enak lokalnemu.
Veliko računalniških jezikov ima določeno mero introspekcije
in refleksije
(prvi izraz se navadno nanaša na tipe, drugi pa je splošnejši; v Pythonu
navadno uporabljamo izraz introspekcija za oboje). Funkciji je dostopen
slovar njenih lokalnih spremenljivk: dobi ga tako, da pokliče funkcijo
locals
.
Ta slovar je mogoče uporabljati tudi za to, da dobimo ali spremenimo vrednost
spremenljivke... Vendar to ni lepo. Že to, da se z if "a" in
locals()
vprašamo, ali obstaja spremenljivka z imenom
a
, je majčkeno na meji. Povem torej samo zato, da boste vedeli,
kako je narejeno.