Zapiski
Lastnosti
Najprej nekoliko nesmiselen primer.
class A:
def stevilka(self):
return 42
Priznam, ta je eden bolj nesmiselnih. Vse, kar lahko naredimo s tem razredom, je
>>> a = A()
>>> a.stevilka()
42
Kaj pa tole?
class A:
@property
def stevilka(self):
return 42
Tudi pred metode lahko postavljamo dekoratorje. Dekorator @property
spremeni funkcijo v getter za lastnosti oz., no, atribute. Z drugimi besedami, s tem stevilka
postane (vsaj, kakor je videti od zunaj) atribut.
>>> a = A()
>>> a.stevilka
42
Razlika med tem atributom in pravim atributom je v tem, da se ta računa, medtem ko pravi atribut "obstaja".
O mehanizmu, kako to deluje, se ne bomo pogovarjali. Le toliko povejmo: dekoratorji ne vračajo nujno funkcij in tale vrača nekaj drugega ... Več v dokumentaciji Pythona.
Na predavanjih iz Programiranja 1 smo menda napisali razred Vector
.
Napišimo razred Vector
s konstruktorjem, ki mu damo komponente (poljubno dimenzionalnega) vektorja.
class Vector:
def __init__(self, *v):
self.values = v
Dajmo tem vektorjem dolžino.
class Vector:
def __init__(self, *v):
self.values = v
@property
def length(self):
from math import sqrt
return sqrt(sum(x ** 2 for x in self.values))
Poskusimo.
>>> v = Vector(3, 4)
>>> v.length
5.0
Zdaj je videti kot da ima Vector
tudi atribut length
. V resnici pa ga vsakič sproti izračuna.
Nekateri pravijo, da bi morali biti vsi atributi dostopni le prek getterjev, prave vrednosti pa bi morale biti privatne. Tako razmišljujoči programerji bi Vector
sprogramirali tako, da bi preimenovali values
v _values
, medtem ko bi values
"ponudili" kot lastnost.
Podčrtaj na začetku imena Pythonu ne pomeni nič posebnega, nam, programerjem, pa pove, da se sme tega atributa dotikati le razred Vector
, tisti, ki uporabljajo ta razred, pa ne. Torej:
class Vector:
def __init__(self, *v):
self._values = v
@property
def length(self):
from math import sqrt
return sqrt(sum(x ** 2 for x in self.values))
@property
def values(self):
return self._values
Navzven se ni spremenilo nič.
>>> v = Vector(3, 4)
>>> v.values
(3, 4)
>>> v.length
5.0
V ozadju pa je values
zdaj lastnost in ne atribut. (Prevodi so zoprna reč. V angleščini govorijo o property in attribute. V slovenščini je oboje lastnost, zato puščam besedo attribute praktično neprevedeno.)
Pravzaprav ni čisto res, da se ni nič spremenilo. Če je values
samo lastnost, je ne moremo nastavljati.
>>> v.values = (5, 6)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
Seveda bi lahko rekli v._values = (5, 6)
, vendar tega ne smemo (čeprav deluje). Ker je tak pač dogovor glede imen, ki se začnejo s podčrtajem.
Tole ima določene prednosti. Recimo, da bi pogosto zahtevali dolžino vektorja. Namesto, da jo računamo vsakič posebej, jo lahko izračunamo enkrat za vselej in shranimo. Morda že kar v konstruktorju.
class Vector:
def __init__(self, *v):
from math import sqrt
self._values = v
self._length = sqrt(sum(x ** 2 for x in self.values))
@property
def values(self):
return self._values
@property
def length(self):
return self._length
Tole sicer ni bistveno drugače kot
class Vector:
def __init__(self, *v):
self._values = v
self.length = sqrt(sum(x ** 2 for x in self.values))
@property
def values(self):
return self._values
vendar počakajmo. Različica, po kateri je length
lastnost, bo še koristna.
Lepota tega je, da bo dolžina vedno pravilna, saj nihče ne more spremeniti values
. (Lahko bi spremenil le _values
in posledično tudi values
, vendar tega ne sme.)
Kaj pa, če želimo omogočiti spreminjanje values
? V tem primeru potrebujemo setter. Getterje in setterje lahko določimo na različne načine. Tule se učimo le enega in skladno z njim se naš program nadaljuje takole
class Vector:
def __init__(self, *v):
self._values = v
self._length = sqrt(sum(x ** 2 for x in self.values))
@property
def values(self):
return self._values
@values.setter
def values(self, new_values):
self._values = new_values
self._length = sqrt(sum(x ** 2 for x in self.values))
@property
def length(self):
return self._length
Uporabimo nek hecen dekorator @values.setter
, s katero okrasimo metodo, ki sprejema argument self
(kot vse metode) in novo vrednost lastnosti, ki jo nastavljamo. Metoda mora nato narediti vse, kar je potrebno narediti,
ko nastavljamo to lastnost. Tule, konkretno, spremeni _values
na novo vrednost, istočasno pa še ažurira self._length
.
Lastne setterje lahko uporabljamo tudi sami. Tako lahko skrajšamo konstruktor.
class Vector:
def __init__(self, *v):
self.values = v
@property
def values(self):
return self._values
@values.setter
def values(self, new_values):
self._values = new_values
self._length = sqrt(sum(x ** 2 for x in self.values))
@property
def length(self):
return self._length
Ker smo v konstruktorje spremenili self._values = v
v self.values = v
, bo prirejanje poklicalo setter, ta pa bo poskrbel še za _length
.
In zdaj še zadnji korak. Kaj pa, če določenega vektorja ne bo nihče vprašal po dolžini? Nekatere druge pa bodo stalno spraševali po dolžini?
class Vector:
def __init__(self, *v):
self.values = v
@property
def values(self):
return self._values
@values.setter
def values(self, new_values):
self._values = new_values
self._length = None
@property
def length(self):
if self._length is None:
self._length = sqrt(sum(x ** 2 for x in self.values))
return self._length
Setter za values
zdaj nastavi _length
na None
, češ, dolžine pa še ne poznamo. Getter za length
preveri, ali je dolžina že izračunana. Če ni (self._length is None
), jo izračuna. Na koncu jo vrne - ne glede na to, ali je izračunana že od prej ali pravkar. Kasnejši klici length
bodo vračali dolžino, ne da bi jo računali. Če kdo slučajno spremeni values
na kako drugo vrednost, pa _length
ponovno postavimo na None
. Ko bo kasneje spet kdo poklical length
, bomo dolžino tako izračunali na novo. Če ne bo nihče spraševal po dolžini, pa je pač ne bomo.