Veľkonočná akcia je tu a s ňou aj extra kredity ZADARMO na náš interaktívny e-learning. Dobij si teraz kredity a posuň sa vo svojej kariére vpred!
Zarábaj až 6 000 € mesačne! Akreditované rekvalifikačné kurzy od 0 €. Viac informácií.

19. diel - Vlastnosti v Pythone - Pokročilé vlastnosti a dedenie

V minulej lekcii, Vlastnosti v Pythone, sme si predstavili vlastnosti alebo gettery a settery, ktoré umožnia jednoduchšie nastavovanie a validáciu hodnôt atribútov.

V dnešnom tutoriáli objektovo orientovaného programovania v Pythone budeme pokračovať v práci s vlastnosťami. Zameriame sa najmä na ich pokročilé použitie. Venovať sa budeme dedeniu, vytváraniu vlastných dekorátorov pre vlastnosti a častým chybám, ktorých sa pri práci s vlastnosťami programátori dopúšťajú.

Pokročilé vlastnosti sú už pomerne náročná téma. Je preto veľmi dôležité starostlivo analyzovať všetky ukážky kódu v lekcii, skúsiť si ich vo vlastnom IDE modifikovať a neprechádzať ďalej v tutoriáli, pokiaľ kód skutočne plne nepochopíte.

Použitie vlastností v dedení

Pozrime sa teda bližšie na dôležitý koncept využitia dekorátora @property v kontexte dedičnosti v Pythone. Dedičnosť umožňuje odvodenej triede zdediť metódy a vlastnosti základnej (rodičovskej) triedy. Pomocou dekorátora @property v základnej triede definujeme vlastnosti, ktoré je potom možné v odvodenej triede preťažovať alebo prispôsobiť:

Klikni pre editáciu
  • class Shape:
        def __init__(self, color='red'):
            self._color = color
    
        @property
        def color(self):
            return self._color
    
        @color.setter
        def color(self, value):
            self._color = value
    
    class Circle(Shape):
        def __init__(self, radius, color='red'):
            super().__init__(color)
            self._radius = radius
    
        @property
        def radius(self):
            return self._radius
    
        @radius.setter
        def radius(self, value):
            if value <= 0:
                raise ValueError("Radius must be greater than 0")
            self._radius = value
    
    
    # Creating an instance of Circle
    circle = Circle(5, "blue")
    
    # Getting and setting properties
    print(f"Circle color: {circle.color}")
    print(f"Circle radius: {circle.radius}")
    
    # Changing properties
    circle.color = "green"
    circle.radius = 10
    
    print(f"New circle color: {circle.color}")
    print(f"New circle radius: {circle.radius}")
    
    # Attempt to set an invalid radius
    try:
        circle.radius = -3
    except ValueError as e:
        print(f"Error: {e}")
    • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

    Trieda Shape je základná trieda, ktorá má vlastnosť color s getterom a setterom. Používa @property na definovanie týchto metód ako vlastností triedy.

    Trieda Circle je odvodená trieda, ktorá dedí z Shape. Zahŕňa svoju vlastnosť radius s vlastným getterom a setterom. Preberá (dedí) zo základnej (rodičovskej) triedy vlastnosť color.

    Tento príklad ukazuje základné použitie @property v dedení. Zároveň ilustruje, ako odvodená trieda rozširuje alebo mení správanie základnej triedy.

    Preťažovanie vlastností v odvodených triedach

    Preťažovanie vlastností (property overriding) v odvodenej triede je proces, kedy nahrádzame alebo rozširujeme chovanie getterov a setterov základnej triedy. Vďaka tomu môžeme dosiahnuť väčšiu flexibilitu a špecializácia v odvodených triedach. Pozrime sa na konkrétnu aplikáciu. V triede Shape máme základnú implementáciu setteru pre farbu, ktorá jednoducho nastavuje hodnotu. V triede Circle teraz pridáme kontrolu, ktorá overí, či zadaná farba patrí do zoznamu vopred definovaných povolených farieb pre kruhy. To je jednoduchý príklad toho, ako v odvodenej triede preťažíme a rozšírime správanie vlastnosti z rodičovskej triedy:

    Klikni pre editáciu
    • class Shape:
          def __init__(self, color='red'):
              self._color = color
      
          @property
          def color(self):
              return self._color
      
          @color.setter
          def color(self, value):
              self._color = value
      
      class Circle(Shape):
          allowed_colors = ['red', 'blue', 'green', 'yellow']
      
          def __init__(self, radius, color='red'):
              super().__init__(color)
              self._radius = radius
      
          @property
          def radius(self):
              return self._radius
      
          @radius.setter
          def radius(self, value):
              if value <= 0:
                  raise ValueError("Radius must be greater than 0")
              self._radius = value
      
          @Shape.color.setter
          def color(self, value):
              if value not in Circle.allowed_colors:
                  raise ValueError(f"The color '{value}' is not allowed for circles.")
              Shape.color.fset(self, value)
      
      # Creating an instance of Circle
      circle = Circle(5, "blue")
      
      # Changing to an allowed color
      circle.color = "yellow"
      print(f"New color of the circle: {circle.color}")
      
      # Attempt to set an unallowed color
      try:
          circle.color = "purple"
      except ValueError as e:
          print(f"Error: {e}")
      • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

      V kóde trieda Circle rozširuje správanie setteru pre farbu. Overuje, či je zadaná farba v zozname povolených farieb. Ak nie, vyvolá výnimku ValueError. Takto je možné v odvodenej triede preťažovať a prispôsobovať vlastnosti základnej triedy.

      Kód je zrejmý až na metódu fset(). To je interná metóda používaná na volanie setteru vlastnosti. Normálne by sme ju v bežnom kóde nevideli, pretože @property ju obvykle skryje a umožňuje nám na volanie setteru používať bežné priradenie atribútu. My ale setter preťažujeme. Preto musíme metódu volať sami. Priame priradenie self.color = value by totiž viedlo k nekonečnej rekurzii.

      Pokročilé vlastnosti a dekorátory

      Vytváranie vlastných dekorátorov pre vlastnosti je jedným z pokročilejších a zároveň užitočných aspektov programovania v Pythone. V tejto kapitole sa pozrieme na to, ako vytvoríme vlastné dekorátory, ktoré je možné použiť pre vlastnosti tried.

      Predstavme si, že chceme vytvoriť dekorátor, ktorý loguje každú zmenu hodnoty vlastnosti (vrátane tých, ktoré spôsobia chybu). V našej triede Circle chceme logovať zmeny polomeru. Najprv si teda napíšeme funkciu pre dekorátor:

      def log_property(radius):                                     # (func)
          def wrapped_function(self, new_value):
              original_value = getattr(self, '_radius')             # (self, '_' + func.__name__)
              if new_value != original_value:
                  print(f"Radius change: {original_value} -> {new_value}")
              return radius(self, new_value)
          return wrapped_function

      Táto funkcia nepatrí do žiadnej triedy a musíme ju do kódu vložiť pred miesto, kde neskôr použijeme dekorátor @log_property. Najlepšie na začiatok súboru alebo medzi triedy Shape a Circle. Funkcia je bohužiaľ pomerne komplikovaná vzhľadom na naše znalosti. Ide hlavne o časť (self, '_' + func.__name__) v komentári kódu. Funkcia getattr() je štandardná vstavaná funkcia v Pythone, ktorá sa používa na dynamické získanie hodnoty atribútu objektu na základe jeho názvu, ktorý je jej odovzdaný ako reťazec. A práve v konštrukcii reťazca je zakopaný jazvečík. Aby bola funkcia univerzálna (a správne napísaná), musela by v atribúte prijímať referenciu (func), nie priamo názov funkcie (radius). Ďalej by sme v getattr() museli reťazec zložiť z podčiarkovníka a názvu funkcie. Práve na zistenie názvu funkcie z referencie func slúži to func.__name__ v komentári. Bohužiaľ, túto látku ešte len budeme preberať. Máme preto funkciu napísanú priamo s napevno vloženými údajmi, a funkcia tak nie je univerzálna. O magických dunder metódach, to sú tie s fakt veľa podčiarkovníky :-D , sa dozvieme neskôr v kurze.

      Určite ale neuškodí, keď si vo svojom IDE skúsime funkciu upraviť na univerzálnu. V komentároch ku kódu je všetko potrebné uvedené.

      Použitie dekorátora

      Dekorátor aplikujeme na setter metódu v našej triede Circle:

      @radius.setter
      @log_property
      def radius(self, value):
          if value <= 0:
              raise ValueError("Radius must be greater than 0")
          self._radius = value

      Kedykoľvek teraz dôjde k zmene polomeru, funkcia dekorátora log_property() túto zmenu zaznamená:

      Klikni pre editáciu
      • # Creating an instance of Circle
        circle = Circle(5, "blue")
        
        circle.radius = 7   # Prints: Radius change: 5 -> 7
        circle.radius = 7   # This line prints nothing because there is no change in value
        circle.radius = 17  # Prints: Radius change: 7 -> 17
        • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

        Vďaka vlastnému dekorátoru teda dokážeme pridávať zložitejšie správanie k vlastnostiam tried bez zásahu do ich vnútornej implementácie. Vlastné dekorátory v praxi obvykle pridávajú dodatočné správanie (napríklad logovanie, overovanie, transformácia dát) k operáciám, ktoré sú spojené s vlastnosťami definovanými pomocou @property. Toto je veľmi mocná vlastnosť jazyka Python, ktorá umožňuje písať čistý, modulárny a ľahko udržiavateľný kód.

        Bežné chyby a nástrahy

        Existuje niekoľko notoricky sa opakujúcich chýb, ktorých sa programátori dopúšťajú. Poďme sa na tie dve hlavné pozrieť bližšie.

        Nekonečné rekurzie pri používaní setterov

        Tejto pasce na nepozorné už sme sa v lekcii dotkli. Nekonečná rekurzia v setteroch nastane, keď setter neúmyselne zavolá sám seba. To sa často stáva, ak sa v settere pre nejakú vlastnosť pokúsime priamo priradiť hodnotu tejto vlastnosti, namiesto toho, aby sme priradili súkromný atribút. Príklad nám to ozrejmí:

        class Circle:
            def __init__(self, radius):
                self.radius = radius    # Calls the setter
        
            @property
            def radius(self):
                return self._radius
        
            @radius.setter
            def radius(self, value):
                if value <= 0:
                    raise ValueError("Radius must be greater than 0")
                self.radius = value  # This will cause infinite recursion! We should have used self._radius with an underscore!

        Interná implementácia triedy má vždy využívať priamy prístup k interným atribútom (self._radius), zatiaľ čo všetok externý prístup má prebiehať cez definované rozhranie (self.radius).

        Poradie dekorátorov

        Poradie, v ktorom sa aplikujú dekorátory, je kľúčové. Už vieme, že dekorátory sa aplikujú odspodu nahor. V prípade kombinácie @property, getteru/setteru a ďalších vlastných dekorátorov je dôležité si uvedomiť, ktorý dekorátor vykoná svoj kód ako prvý a ako to ovplyvní ďalšie správanie kódu. Napríklad, ak máme vlastný dekorátor pre logovanie a chceme ho použiť spoločne s @property, musí sa @property spustiť ako posledný. Tým zaistíme, že logovanie bude zachytávať operácie na úrovni vlastnosti, nie na úrovni metódy:

        class Circle:
            @log_property  # This decorator is executed first
            @property      # Then the @property will be executed
            def radius(self):
                return self._radius

        Keď kód pristupuje k vlastnosti radius, najskôr sa aktivuje správanie dekorátora @log_property (pretože je napísaný hore a spúšťa sa ako prvý). Až potom sa vykoná predvolená operácia gettera definovaného @property dekorátorom.

        Dekorátory sa najprv aplikujú vo vzostupnom poradí, ale spúšťajú sa v zostupnom poradí.

        Zdrojový kód z lekcie je na stiahnutie v archíve :-)

        V budúcej lekcii, Magické metódy v Pythone, sa pozrieme na magické metódy objektov.


         

        Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

        Stiahnuť

        Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

        Stiahnuté 1x (1.43 kB)
        Aplikácia je vrátane zdrojových kódov v jazyku Python

         

        Ako sa ti páči článok?
        Pred uložením hodnotenia, popíš prosím autorovi, čo je zleZnakov 0 z 50-500
        Predchádzajúci článok
        Vlastnosti v Pythone
        Všetky články v sekcii
        Objektovo orientované programovanie v Pythone
        Preskočiť článok
        (neodporúčame)
        Magické metódy v Pythone
        Článok pre vás napísal Karel Zaoral
        Avatar
        Užívateľské hodnotenie:
        10 hlasov
        Karel Zaoral
        Aktivity