Python - @property dekorator


Klase bez Getters i Setters

Pretpostavimo da smo odlučili da kreiramo klasu koja čuva temperaturu u Celzijusovim stepenima, ali takođe bi željeli da implementirao metodu za pretvaranje temperature u stepene Fahrenheita, ali kako to da uradimo? Jedan od načina za to je sljedeći:

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

Možemo napraviti predmete od ove klase i manipulisati atributom temperature kako želimo:

# Osnovni metod postavljanja i dobivanja atributa u Pythonu
class Celsius:
    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32


# Kreiranja novog objekta
human = Celsius()

# Postavljanje temperature
human.temperature = 37

# Dobijanje atributa temperature
print(human.temperature)

# Dobijanje metode to_fahrenheit
print(human.to_fahrenheit())

Dodatne decimale pri konverziji u Fahrenheit posljedica su aritmetičke greške s pomičnim zarezom (floating point). Da biste saznali više, posjetite lekciju Python - Brojevi, tipovi konverzacije i matematika. Kad god dodijelimo ili dohvatimo bilo koji atribut objekta poput temperature kao što je prikazano u gornjem primjeru, Python ga pretražuje u ugrađenom atributu __dict__ rječnika.

>>> human.__dict__
{'temperature': 37}

Zbog toga man.temperature unutra postaje man.__dict__['temperatura'].



Korištenje Getters i Setters

Pretpostavimo da želimo proširiti upotrebljivost klase Celsius definisane u prehodnom primjeru. Znamo da temperatura bilo kog objekta ne može dostići ispod -273,15 stepeni Celzijusa (apsolutna nula u termodinamici). Ažurirajmo naš kod kako bismo implementovali ovo ograničenje vrijednosti.

Očito rješenje za gornje ograničenje biće sakriti temperaturu atributa (učiniti je privatnom) i definisati nove metode getter i setter kako bi se njome manipulisalo. To se može učiniti na sljedeći način:

# Kreiranje Getters i Setter metode
class Celsius:
    def __init__(self, temperature=0):
        self.set_temperature(temperature)

    def to_fahrenheit(self):
        return (self.get_temperature() * 1.8) + 32

    # getter metoda
    def get_temperature(self):
        return self._temperature

    # setter metoda
    def set_temperature(self, value):
        if value < -273.15:
            raise ValueError("Temperatura ispod -273.15 nije moguća.")
        self._temperature = value

Kao što vidimo, gornja metoda uvodi dvije nove metode get_temperature() i set_temperature(). Dalje, temperatura je zamijenjena sa _temperature. Podvlaka _ na početku koristi se za označavanje privatnih varijabli u Pythonu. Sada, iskoristimo ovu implementaciju:

# Kreiranje Getters i Setter metode
class Celsius:
    def __init__(self, temperature=0):
        self.set_temperature(temperature)

    def to_fahrenheit(self):
        return (self.get_temperature() * 1.8) + 32

    # getter metoda
    def get_temperature(self):
        return self._temperature

    # setter metoda
    def set_temperature(self, value):
        if value < -273.15:
            raise ValueError("Temperatura ispod -273.15 nije moguća.")
        self._temperature = value


# Kreiranje novog objekta, set_temperature() i interno pozivanje __init__
human = Celsius(37)

# Dobiti atribut temperature putem metode getter
print(human.get_temperature())

# Dobijanje metodu to_fahrenheit, get_temperature () koju poziva sama metoda
print(human.to_fahrenheit())

# Nova primjena ograničenja
human.set_temperature(-300)

# Dobijanje to_fahreheit metode
print(human.to_fahrenheit())

Ovo ažuriranje uspješno je primijenilo novo ograničenje. Više ne smijemo postavljati temperaturu ispod -273,15 stepeni Celzijusa.

Međutim, veći problem s gornjim ažuriranjem je taj što svi programi koji su implementovali našu prethodnu klasu moraju modifikovati svoj kod iz obj.temperature u obj.get_temperature() i sve izraze poput obj.temperature = val u obj.set_temperature(val). Ova refaktorizacija može stvoriti probleme dok se bavite stotinama hiljada linija koda. Sve u svemu, naše novo ažuriranje nije bilo unazad kompatibilno. Ovdje @property dolazi u pomoć.



Klasa property

Pitetni način rješavanja gornjeg problema je upotreba klase property. Evo kako možemo ažurirati naš kod:

# korištenje property klase
class Celsius:
    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    # getter
    def get_temperature(self):
        print("Getting vrijednost...")
        return self._temperature

    # setter
    def set_temperature(self, value):
        print("Setting vrijednost...")
        if value < -273.15:
            raise ValueError("Temperatura ispod -273.15 nije moguća")
        self._temperature = value

    # kreiranje property objekta
    temperature = property(get_temperature, set_temperature)

Dodali smo funkciju print() unutar get_temperature() i set_temperature() kako bismo jasno uočili da se izvršavaju. Posljednji red koda daje temperaturu objekta property. Jednostavno rečeno, svojstvo pridružuje neki kod (get_temperature i set_temperature) pristupima atributa člana (temperature). Koristimo ovaj kod za ažuriranje:

# korištenje property klase
class Celsius:
    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    # getter
    def get_temperature(self):
        print("Getting vrijednost...")
        return self._temperature

    # setter
    def set_temperature(self, value):
        print("Setting vrijednost...")
        if value < -273.15:
            raise ValueError("Temperatura ispod -273.15 nije moguća")
        self._temperature = value

    # kreiranje property objekta
    temperature = property(get_temperature, set_temperature)

human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
human.temperature = -300

Kao što možemo vidjeti, bilo koji kod koji preuzme vrijednost temperature automatski će pozvati get_temperature() umjesto pretraživanja rječnika (__dict__). Slično tome, bilo koji kod koji temperaturi dodijeli vrijednost automatski će pozvati set_temperature(). Gore možemo čak vidjeti da je set_temperature() bio pozvan čak i kad smo kreirali objekt.

>>> human = Celsius(37)
Setting vrijednost...

Možete li pogoditi zašto?

Razlog je taj što se prilikom kreiranja objekta poziva metoda __init__(). Ova metoda ima liniju self.temperature = temperature. Ovaj izraz automatski poziva set_temperature(). Slično tome, svaki pristup poput c.temperature automatski poziva get_temperature(). To je ono šta property radi. Evo još nekoliko primjera.

>>> human.temperature
Getting vrijednost
37
>>> human.temperature = 37
Setting vrijednost

>>> c.to_fahrenheit()
Getting vrijednost
98.60000000000001

Korištenjem svojstva property možemo vidjeti da nije potrebna nikakva izmjena u implementaciji ograničenja vrijednosti. Zbog toga je naša implementacija kompatibilna unazad (backward).



@property decorator

U Pythonu, property() je ugrađena funkcija koja kreira i vraća objekt property. Sintaksa ove funkcije je:

property(fget=None, fset=None, fdel=None, doc=None)

Ovdje je:

  • fget - je funkcija za dobijanje vrijednosti atributa
  • fset - je funkcija za postavljanje vrijednosti atributa
  • fdel - je funkcija za brisanje atributa
  • doc - je string (poput komentara)

Kao što se vidi iz implementacije, ovu nisu argumenti funkcije, već su opcionalni. Dakle, objekt property može se jednostavno kreirati na sljedeći način.

>>> property()
<property object at 0x0000000003239B38>

Objekt svojstva ima tri metode: getter(), setter() i deleter() da bi kasnije odredili: fget, fset i fdel. To znači, linija:

temperature = property(get_temperature,set_temperature)

Može se podijeliti kao:

# kreiranje praznog  property
temperature = property()
#  dodijela fget
temperature = temperature.getter(get_temperature)
# dodijela fset
temperature = temperature.setter(set_temperature)

Ova dva dijela koda su ekvivalentna. Programeri upoznati s Python Decorators mogu prepoznati da se gornja konstrukcija može implementovati kao dekoratori. Čak ne možemo definisati imena get_temperature, i set_temperature, jer su nepotrebna i narušavaju prostor imena klase. U tu svrhu ponovo koristimo naziv temperature dok definišemo naše funkcije getter i setter . Pogledajmo kako to implementovati kao dekorator:

# Korištenje @property decoratora
class Celsius:
    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    @property
    def temperature(self):
        print("Getting vrijednost...")
        return self._temperature

    @temperature.setter
    def temperature(self, value):
        print("Setting vrijednost...")
        if value < -273.15:
            raise ValueError("Temperatura ispod -273 nije moguća")
        self._temperature = value

# Kreiranje objekta
human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
coldest_thing = Celsius(-300)

Gornja implementacija je jednostavna i efikasna. Preporučen je način korištenja property-a.