Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 19 - Vlastnosti v Pythonu podruhé - Pokročilé vlastnosti a dědění

V minulé lekci, Vlastnosti v Pythonu, jsme si představili vlastnosti neboli gettery a settery, které umožní snazší nastavování a validaci hodnot atributů.

V dnešním tutoriálu objektově orientovaného programování v Pythonu budeme pokračovat v práci s vlastnostmi. Zaměříme se zejména na jejich pokročilé užití. Věnovat se budeme dědění, vytváření vlastních dekorátorů pro vlastnosti a častým chybám, kterých se při práci s vlastnostmi programátoři dopouští.

Pokročilé vlastnosti jsou již poměrně náročné téma. Je proto velmi důležité pečlivě analyzovat všechny ukázky kódu v lekci, zkusit si je ve vlastním IDE modifikovat a nepřecházet dál v tutoriálu, dokud kód skutečně plně nepochopíte.

Použití vlastností v dědění

Podívejme se tedy blíže na důležitý koncept využití dekorátoru @property v kontextu dědičnosti v Pythonu. Dědičnost umožňuje odvozené třídě zdědit metody a vlastnosti základní (rodičovské) třídy. Pomocí dekorátoru @property v základní třídě definujeme vlastnosti, které je potom možné v odvozené třídě přetěžovat nebo přizpůsobit:

class Tvar:
    def __init__(self, barva='červená'):
        self._barva = barva

    @property
    def barva(self):
        return self._barva

    @barva.setter
    def barva(self, hodnota):
        self._barva = hodnota

class Kruh(Tvar):
    def __init__(self, polomer, barva='červená'):
        super().__init__(barva)
        self._polomer = polomer

    @property
    def polomer(self):
        return self._polomer

    @polomer.setter
    def polomer(self, hodnota):
        if hodnota <= 0:
            raise ValueError("Poloměr musí byt větší než 0")
        self._polomer = hodnota

# Vytvoření instance Kruh
kruh = Kruh(5, "modrá")

# Získání a nastavení vlastností
print(f"Barva kruhu: {kruh.barva}")
print(f"Poloměr kruhu: {kruh.polomer}")

# Změna vlastností
kruh.barva = "zelená"
kruh.polomer = 10

print(f"Nová barva kruhu: {kruh.barva}")
print(f"Nový poloměr kruhu: {kruh.polomer}")

# Pokus o nastavení neplatného poloměru
try:
    kruh.polomer = -3
except ValueError as e:
    print(f"Chyba: {e}")

Třída Tvar je základní třída, která má vlastnost barva s getterem a setterem. Používá @property pro definování těchto metod jako vlastnosti třídy.

Třída Kruh je odvozená třída, která dědí z Tvar. Zahrnuje svou vlastnost polomer s vlastním getterem a setterem. Přebírá (dědí) ze základní (rodičovské) třídy vlastnost barva.

Tento příklad ukazuje základní použití @property v dědění. Zároveň ilustruje, jak odvozená třída rozšiřuje nebo mění chování základní třídy.

Přetěžování vlastností v odvozených třídách

Přetěžování vlastností (property overriding) v odvozené třídě je proces, kdy nahrazujeme nebo rozšiřujeme chování getterů a setterů základní třídy. Díky tomu můžeme dosáhnout větší flexibility a specializace v odvozených třídách. Podívejme se na konkrétní aplikaci. Ve třídě Tvar máme základní implementaci setteru pro barvu, která jednoduše nastavuje hodnotu. V třídě Kruh teď přidáme kontrolu, která ověří, zda zadaná barva patří do seznamu předem definovaných povolených barev pro kruhy. To je jednoduchý příklad toho, jak v odvozené třídě přetížíme a rozšíříme chování vlastnosti z rodičovské třídy:

class Tvar:
    def __init__(self, barva='červená'):
        self._barva = barva

    @property
    def barva(self):
        return self._barva

    @barva.setter
    def barva(self, hodnota):
        self._barva = hodnota

class Kruh(Tvar):
    povolene_barvy = ['červená', 'modrá', 'zelená', 'žlutá']

    def __init__(self, polomer, barva='červená'):
        super().__init__(barva)
        self._polomer = polomer

    @property
    def polomer(self):
        return self._polomer

    @polomer.setter
    def polomer(self, hodnota):
        if hodnota <= 0:
            raise ValueError("Poloměr musí být větší než 0")
        self._polomer = hodnota

    @Tvar.barva.setter
    def barva(self, hodnota):
        if hodnota not in Kruh.povolene_barvy:
            raise ValueError(f"Barva '{hodnota}' není pro kruhy povolená.")
        Tvar.barva.fset(self, hodnota)

# Vytvoření instance Kruh
kruh = Kruh(5, "modrá")

# Změna barvy na povolenou barvu
kruh.barva = "žlutá"
print(f"Nová barva kruhu: {kruh.barva}")

# Pokus o nastavení nepovolené barvy
try:
    kruh.barva = "fialová"
except ValueError as e:
    print(f"Chyba: {e}")

V kódu třída Kruh rozšiřuje chování setteru pro barvu. Ověřuje, zda je zadaná barva v seznamu povolených barev. Pokud ne, vyvolá výjimku ValueError. Takto lze v odvozené třídě přetěžovat a přizpůsobovat vlastnosti základní třídy.

Kód je zřejmý až na metodu fset. To je interní metoda používaná pro volání setteru vlastnosti. Normálně bychom ji v běžném kódu neviděli, protože @property ji obvykle skryje a umožňuje nám pro volání setteru používat běžné přiřazení atributu. My ale setter přetěžujeme. Proto musíme metodu volat sami. Přímé přiřazení self.barva = hodnota by totiž vedlo k nekonečné rekurzi.

Pokročilé vlastnosti a dekorátory

Vytváření vlastních dekorátorů pro vlastnosti je jedním z pokročilejších a zároveň užitečných aspektů programování v Pythonu. V této kapitole se podíváme na to, jak vytvoříme vlastní dekorátory, které lze použít pro vlastnosti tříd.

Představme si, že chceme vytvořit dekorátor, který loguje každou změnu hodnoty vlastnosti (včetně těch, které způsobí chybu). V naší třídě Kruh chceme logovat změny poloměru. Nejprve si tedy napíšeme funkci pro dekorátor:

def log_property(polomer):                                     # (func)
    def obalena_funkce(self, nova_hodnota):
        puvodni_hodnota = getattr(self, '_polomer')            # (self, '_' + func.__name__)
        if nova_hodnota != puvodni_hodnota:
            print(f"Změna poloměru: {puvodni_hodnota} -> {nova_hodnota}")
        return polomer(self, nova_hodnota)
    return obalena_funkce

Tato funkce nepatří do žádné třídy a musíme ji do kódu vložit před místo, kde později použijeme dekorátor @log_property. Nejlépe na začátek souboru nebo mezi třídy Tvar a Kruh. Funkce je bohužel poměrně komplikovaná vzhledem k našim znalostem. Jde hlavně o část (self, '_' + func.__name__) v komentáři kódu. Funkce getattr() je standardní vestavěná funkce v Pythonu, která se používá k dynamickému získání hodnoty atributu objektu na základě jeho názvu, který je jí předán jako řetězec. A právě v konstrukci řetězce je zakopaný jezevčík. Aby byla funkce univerzální (a správně napsaná), musela by v atributu přijímat referenci (func), ne přímo název funkce (polomer). Dále bychom v getattr() museli řetězec složit z podtržítka a názvu funkce. Právě ke zjištění názvu funkce z reference func slouží to func.__name__ v komentáři. Bohužel, tuto látku teprve budeme probírat. Máme proto funkci napsanou přímo s napevno vloženými údaji, a funkce tak není univerzální. O magických dunder metodách, to jsou ty s fakt hodně podtržítky :-D, se dozvíme později v kurzu.

Určitě ale neuškodí, když si ve svém IDE zkusíme funkci upravit na univerzální. V komentářích ke kódu je vše potřebné uvedeno.

Použití dekorátoru

Dekorátor aplikujeme na setter metodu v naší třídě Kruh:

@polomer.setter
@log_property
def polomer(self, hodnota):
    if hodnota <= 0:
        raise ValueError("Poloměr musí být větší než 0")
    self._polomer = hodnota

Kdykoliv teď dojde ke změně poloměru, funkce dekorátoru log_property() tuto změnu zaznamená:

# Vytvoření instance Kruh
kruh = Kruh(5, "modrá")

kruh.polomer = 7  # Vypíše: Změna poloměru: 5 -> 7
kruh.polomer = 7  # Tento řádek nevypíše nic, protože nedochází ke změně hodnoty
kruh.polomer = 17  # Vypíše: Změna poloměru: 7 -> 17

Díky vlastnímu dekorátoru tedy dokážeme přidávat složitější chování k vlastnostem tříd bez zásahu do jejich vnitřní implementace. Vlastní dekorátory v praxi obvykle přidávají dodatečné chování (například logování, ověřování, transformace dat) k operacím, které jsou spojeny s vlastnostmi definovanými pomocí @property. Toto je velmi mocná vlastnost jazyka Python, která umožňuje psát čistý, modulární a snadno udržovatelný kód.

Běžné chyby a nástrahy

Existuje několik notoricky se opakujících chyb, kterých se programátoři dopouštějí. Pojďme se na ty dvě hlavní podívat blíže.

Nekonečné rekurze při používání setterů

Této pasti na nepozorné už jsme se v lekci dotkli. Nekonečná rekurze v setterech nastane, když setter neúmyslně zavolá sám sebe. To se často stává, jestliže se v setteru pro nějakou vlastnost pokusíme přímo přiřadit hodnotu této vlastnosti, místo abychom přiřadili soukromý atribut. Příklad nám to ozřejmí:

class Kruh:
    def __init__(self, polomer):
        self.polomer = polomer  # Volá setter

    @property
    def polomer(self):
        return self._polomer

    @polomer.setter
    def polomer(self, hodnota):
        if hodnota <= 0:
            raise ValueError("Poloměr musí být větší než 0")
        self.polomer = hodnota  # ZDE nastane nekonečná rekurze! Měli jsme použít self._polomer s podtržítkem!

Interní implementace třídy má vždy využívat přímý přístup k interním atributům (self._polomer), zatímco veškerý externí přístup má probíhat přes definované rozhraní (self.polomer).

Pořadí dekorátorů

Pořadí, ve kterém se aplikují dekorátory, je klíčové. Už víme, že dekorátory se aplikují odspodu nahoru. V případě kombinace @property, getteru/setteru a dalších vlastních dekorátorů je důležité si uvědomit, který dekorátor provede svůj kód jako první a jak to ovlivní další chování kódu. Například, pokud máme vlastní dekorátor pro logování a chceme ho použít společně s @property, musí se @property spustit jako poslední. Tím zajistíme, že logování bude zachytávat operace na úrovni vlastnosti, nikoliv na úrovni metody:

class Kruh:
    @log_property  # Tento dekorátor se spustí jako první
    @property      # Poté bude spuštěn @property
    def polomer(self):
        return self._polomer

Když kód přistupuje k vlastnosti polomer, nejdříve se aktivuje chování dekorátoru @log_property (protože je napsán nahoře a spouští se jako první). Až poté se provede výchozí operace getteru definovaného @property dekorátorem.

Dekorátory se nejprve aplikují ve vzestupném pořadí, ale spouštějí se v sestupném pořadí.

Zdrojový kód z lekce je ke stažení v archivu :-)

V příští lekci, Magické metody v Pythonu, se podíváme na magické metody objektů.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 24x (1.87 kB)
Aplikace je včetně zdrojových kódů v jazyce Python

 

Předchozí článek
Vlastnosti v Pythonu
Všechny články v sekci
Objektově orientované programování v Pythonu
Přeskočit článek
(nedoporučujeme)
Magické metody v Pythonu
Článek pro vás napsal Karel Zaoral
Avatar
Uživatelské hodnocení:
96 hlasů
Karel Zaoral
Aktivity