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?

a = 1 b = "Benjamin" c = 2 + a

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

t = [1, 2, 3]
Dobili bomo, kar kaže slika na desni: tri objekte, 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.
t = [1, 2, 3]
Kadar bodo v seznamih kake druge stvari, pa bomo risali, tako kot je res - iz seznama bo vodila puščica na objekt (ali objekte), ki jih vsebuje.

Primer 1

Sestavimo tri sezname, t, u in v.

t = [1, 2, 3] u = t v = t[:]

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 == u True >>> t == v True Niso pa isti. Imeni 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. >>> t is u True >>> t is v False

Zdaj seznamu u dodajmo še en element. Kaj dobimo?

u.append(4)
S tem, ko smo spreminjali 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.

u = []

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.

e = [] t = [e]

Da sta e in ničti element t, t[0], res eno in isto, se hitro prepričamo.

>>> e is t[0] True

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.

e.append(1)
Izpišimo t, da bomo videli, da je res. >>> t [[1]] In obratno, kar počnemo s t[0], se zgodi tudi z e.
t[0].append(2)
Zdaj izpišimo e, da vidimo, da se je res spremenil. >>> e [1, 2] Za konec, tako kot v prejšnjem primeru, spremenimo e.
e = []
Čemu prečrtana beseda? Ker je dvoumna. Ime 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: >>> t [1, 2]

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.

e = [] t = [e]*3
Ko smo nekje na začetku rekli, da Python ne kopira objektov, smo mislili resno: t ne vsebuje treh praznih seznamov, temveč trikrat vsebuje isti prazen seznam, znan tudi pod imenom e. >>> t[0] is e True >>> t[1] is e True >>> t[2] is e True

Č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.

e.append(1)

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:

>>> t [[1], [1], [1]]

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.

e = [] t = [e]*3 t.append([])
Bistvo vaje je v tem, da t sicer vsebuje štiri prazne sezname, vendar so prvi trije isti, zadnji pa le enak. >>> t [[], [], [], []] >>> t[0] is e True >>> t[1] is e True >>> t[2] is e True >>> t[3] is e False

Č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.

e.append(1)
>>> t [[1], [1], [1], []]

Primer 5

More seznam vsebovati sam sebe? Tega ni težko preskusiti.

t = [1, 2, 3] t.append(t)

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.

>>> len(t) 4 Element z indeksom tri je seveda t sam: >>> t[3] is t True Pa ga lahko izpišemo? Do neke mere. ;) >>> t [1, 2, 3, [...]] Python je zvit. Tretjega elementa ne izpisuje, saj ve, kaj bi se zgodilo potem. Pa ga lahko, ta, tretji element, izpišemo sami? >>> t[3] [1, 2, 3, [...]] Jasno, tretji element je tako ali tako t sam: če izpišemo t ali t[3], je to eno in isto.

Pa dodajmo v t še en element.

t.append(5)
Zdaj ima 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č. >>> t [1, 2, 3, [...], 5] >>> len(t) 5 >>> len(t[3]) 5

Primer 6

Definirajmo funkcijo, ki sprejme dva argumenta in ju malo spremeni.

def f(a, b): a = 2 b.append(3)

Vzemimo zdaj eno številko in en seznam.

x = 1 y = []

Pokličimo funkcijo, kot argumente ji dajmo x in y.

f(x, y)
Zdaj vidimo, kaj so pravzaprav argumenti funkcije, 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 = 2
Smo s tem spreminjali 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.append(3)
Tule v resnici spreminjamo 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 >>> x 1 >>> y [3]

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.

>>> a = 0 >>> b = a >>> a += 1 >>> b 0 >>> a = [] >>> b = a >>> a += [42] >>> b [42]

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.

def je_popolno(n): v = 0 i = 1 while i < n: if n % i == 0: v += i i += 1 return v def vsa_popolna(): v = [] i = 1 while i <= 1000: if je_popolno(i): v.append(i) i += 1 return v

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.

def f(): print("f", x) def g(): x = 13 print("g", x) x = 42 f() g() print("*", x)

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.

def f(): x.append(1) x = [] f() Pač pa funkcije ne morejo prirejati objektov globalnim imenom. Spodnja funkcija ne spreminja objekta, na katerega se nanaša globalni seznam x: def f(): x = [1] x = [] f()

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.

>>> def f(x): ... a = 42 ... b = "Benjamin" ... print(locals()) ... >>> f(333) {'a': 42, 'x': 333, 'b': 'Benjamin'}

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.

Last modified: Sunday, 2 January 2022, 7:27 PM