Skip to content

Instantly share code, notes, and snippets.

@4e1e0603
Last active April 4, 2021 15:14
Show Gist options
  • Save 4e1e0603/d6804aa6068b23cc746c to your computer and use it in GitHub Desktop.
Save 4e1e0603/d6804aa6068b23cc746c to your computer and use it in GitHub Desktop.
Using Properties In Python

Jak a proč v Pythonu využívat properties

V tomto článku si vysvětlíme k čemu jsou properties, jak jich využít, jaké výhody přináší a proč bychom je měli používat namísto přímého přístupu k proměnným nebo namísto tzv. get/set metod, které se běžné používají v jiných OOP jazycích.


V následujícím příkladu vytvoříme třídu A s jedním instančním atributem x a výchozí hodnotou 0.

class A:
    def __init__(self, x=0):
        self.x = x

Vytvoříme si objekt a změníme výchozí hodnotu atributu:

a = A()      #<< x je 0
a.x = 1      #<< x je 1
print(a.x)  

Všechno funguje do té doby, než budeme chtít zavést nějaké omezení pro členskou proměnno (atribut) x, např. můžeme požadovat, aby hodnota byla nezáporná. V incializační funkci (konstruktoru) bychom mohli napsat takovouto podmínku:

class A:
    def __init__(self, x=0):
        if x < 0:
            raise ValueError("Hodnota `x` musí být nezáporná!")
        self.x = x

Následující dva objekty jsou bezproblému vytvořeny, ale třetí pokus o vytvoření objektu vyvolá (očekávaně) výjimku při pokusu o nepovolenou inicializaci atributu:

a1 = A()     #<< OK
print(a1.x)  #>> 0

a2 = A(1)    #<< OK
print(a2.x)  #>> 1

a3 = A(-1)   #>> ValueError

Nicméně uživatel využívajicí objekt má přímý přístup k proměnné a nic mu nezabrání tuto hodnotu po bezproblémovém vytvoření objektu změnit!

To znamená, že v každé metodě, která počítá s nezápornou hodnotou, by se, pokud bysme v nich hodnotu x kontrolovali, v lepším případě vyvolala výjimka, v horším případě se začnou dít v našem programu zvláštní věci, pokud žádná kontrola v metodách není.

Nemůžeme však od všech metod, které spoléhají na validni hodnotu x požadovat kontrolu, pro případ, že by někdo změnil její hodnotu až po inicializaci objektu!

a1.x = -1  #<< :( Každá metoda používající `x` je nyní v ohrožení! 

Použití chráněných atributů

Řešením by mohlo být nějaké omezení pro přístup k této proměnné. Můžeme proměnnou prefixovat jedním nebo dvěma podtržítky (_x nebo __x) a vytvořit metody pro práci s členskou proměnnou tzv. getter a setter. Pak se, při každém pokusu o změnu hodnoty x validace přesune tam, přesněji řečeno do set metody.

Proč se pro vrácení hodnoty atributu může hodit i get metoda, vysvětlíme dále (viz líne vyhodnocení).

Poznámka:


Je nutno poznamenat, že prefix _ uživatele pouze upozorňuje, že není vhodné manipulovat s proměnnou přímo, ale pomocí nějaké metody -- to se týka tedy hlavně nastavení proměnné. Stále k ní může přistupovat bez varování! Jedná se tedy pouze o konvenci a uživatel může ignorovat naše get/set metody!

class A:
    def __init__(self, x=0):
        self._x = x

a = A(1)
print(a.x)  #>> 1
a._x = -1   #<< Uživateli by měl být na pozoru při práci s proměnnou `_x`.

Prefixování se dvěma podtržítky je stále pouze konvence, ale uživatel je již interpreterem důrazně odmítnut!

class A:
    def __init__(self, x=0):
        self.__x = x

a = A()    #<< `__x` je 0
a.__x = 1  #>> AttributeError: 'A' object has no attribute '__x'

Nicméně i to se dá obejít, pokud víme, jak Python používá tzv. komolení jmen (name-mangling).


Použití get/set metod

Nyní si tedy ukážeme, jak skrýt atribut instance a zpřístupnit ho pomocí get/set metod, které známe např. z jazyků Java a C++. Atribut budeme prefixovat dvěma podtržítky, abychom uživatele donutili používat naše metody pro přístup.

class A:
    def __init__(self, x=0):
        self.__x = x

    def set_x(self, x):
        if x < 0:
            raise ValueError("Hodnota `x` musí být nezáporná!")
        self.__x = x

    def get_x(self):
        return self.__x

a = A()          
print(a.get_x())  #>> 0

a.set_x(1)        
print(a.get_x())  #>> 1

a.set_x(-1)       #>> ValueError...

Líné vyhodnocení

Zřejmě vidíte, že setter přínáší jakousi výhodu -- validuje atribut. Zřejmě však přemýšlíte nad tím, jaké výhody přináší getter.

Pokud ho používáme jen pro vrácení hodnoty, tak vpodstatě žádný!

Představme si však, že chceme využít tzv. líného vyhodnocení (lazy evaluation). Pokud máme atribut jehož hodnota je výsledkem nějakého složitějšího výpočtu, můžeme jeho hodnotu vypočíst až při volání get metody, namísto při inicializaci objektu.

Následně si ji uložit do atributu a vracet případně již tuto předpočítanou hodnotu, dokud se nezmění nějaké hodnoty, ze kterých se atribut počítá.

Nebo také, pokud není výpočet náročný, ale generuje nějakou objemnou datovou strukturu, kterou není nutné uchovávat, můžeme ji vždý vytvořit na požádání a nádledně zahodit.

(Tyto poznámky by si zasloužily konkrétní příklady, ale snad je to pochopitelné.)


Při použití get/set metod se zdá vše vyřešeno, jenže je tu ještě jedna možnost, jak tento problém řešit, hodně lidí bude souhlasit, že mnohem elegantněji a blíže filozofii Pythonu.

Použití properties

Properties umožňují to samé, co get/set metody, ale vypadají a používají se mnohem lépe a uživatele třídy odstiňují od přímého použití dodatečných metod.

class A:
    def __init__(self, x=0):
        if x < 0:
            raise ValueError("---")
        self.__x = x

    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, value):
        if x < 0:
            raise ValueError("---")
        self.__x = x

První metoda za inicializační funkcí __init__, označená dekorátorem @property je vpodstatě getter a druhá, jak už poznáme z dekorátoru @x.setter je setter. Výhoda tohoto přístupu je v tom, že uživatel přistupuje k atributu __x, zdánlivě jako k veřejnému atributu x, tedy tak, jsme si obě metody pojmenovali.

a = A()     
print(a.x)  #>> 0

a.x = 1
print(a.x)  #>> 1 

Uživatel tedy vůbec neví, že nepoužívá veřejný atribut! Představme si situaci, kdy má třída veřejný atribut, který uživatel používá ve svém programu. Pokud se dodatečně rozhodneme, že je třeba hodnotu nějak validovat, uživatel nemusí měnit způsob práce s tímto atributem.

Konkrétně, bysme chtěli dát našemu atributu popisnější hodnotu např. value namísto x.) Pokud používáme properties, pak uživatel může stále volat -- pracovat s x. Tak nám stačí jen změnit název atributu uvnitř příslušných metod. (Pro stručnost vynechávám validaci hodnoty).

class A:
    def __init__(self, value):
        self.__value = value  #<< dříve x

    @property
    def x(self):
        return self.__value

    @x.setter
    def x(self, value)
        self.__value = value 

Uživatel stále volá a.x, i když jsme změnili název atributu.

(Je vpodstatě jedno jak pojmenujeme předávaný parametr v def x(self, param).)

Jaký je rozdíl oproti get/set metodám?

Pokud jde o možnost přidat validaci, využít líné vyhodnocení nebo změnit dodatečně název atributu, pak jsou tyto dva přístupy stejné.

Představme si však, že začneme psát náš program s nejjednodušší možnou implementací, a to pomocí veřejných atributů. Naše atributy jsou jednoduché proměnné uchovávající hodnotu a nemáme potřebu je nijak validovat. Požadavek na validaci, nebo líné vyhodnocení pro úsporu zdrojů se objeví až v průběhu vývoje, kdy už třídu používá jiná část programu.

Pak stačí jen přidat příslušné properties! Nikdo z uživatelů třídy není tímto postižen. Pokud bysme dodatečně přidávali getter/setter mění si uživateli veřejné rozhranní třídy! Kromě jisté větší elegance je toto hlavní výhoda používání properties.

Poznámka:

Použití properties je obdobné, i když s malými rozdíly např, v jazyce C# -- výhody jejich použití však zůstávají.

--David Landa

Poznámka na konec

Pokud jakoukoliv metodu dekorujeme jako @property, ale metoda nepracuje s instančním atributem, je dekorátor v podstatě zbytečný a chová se jako obyčejný atribut.

class A:
    def __init__(self, x=0):
        pass  #<< nevytváříme žádný atribut

    @property
    def x(self):
        return 10 * 10

Můžeme tedy s klidem získat jeho hodnotu.

a = A()
print(a.x) #>> 100 

Zkuste však přidat @x.setter..., nebude to fungovat!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment