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.

Zadnja sprememba: nedelja, 10. april 2016, 15.56