NOVINKA: Získej 40 hodin praktických dovedností s AI – ZDARMA ke každému akreditovanému kurzu!
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 20 - 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řepisovat nebo přizpůsobit:

Klikni pro editaci
  • 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}")
    • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

    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řepisování vlastností v odvozených třídách

    Přepisová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řepíšeme a rozšíříme chování vlastnosti z rodičovské třídy:

    Klikni pro editaci
    • 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}")
      • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

      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řepisovat 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řepisujeme. 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á:

      Klikni pro editaci
      • # 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
        • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

        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 139x (1.87 kB)
        Aplikace je včetně zdrojových kódů v jazyce Python

         

        Jak se ti líbí článek?
        Před uložením hodnocení, popiš prosím autorovi, co je špatněZnaků 0 z 50-500
        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í:
        328 hlasů
        Karel Zaoral
        Aktivity