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í.

17. diel - Dekorátory druhýkrát - Parametrické a triedne dekorátory

V predchádzajúcej lekcii, Dekorátory v Pythone, sme sa zoznámili s dekorátormi a vysvetlili si princíp ich použitia.

V dnešnom tutoriáli objektovo orientovaného programovania v Pythone budeme pokračovať v práci s dekorátormi. Naučíme sa ich parametrizovať a aplikovať na triedu. Na záver lekcie si tému zhrnieme a ukážeme si celý postup vytvorenia dekorátora v navazujúcich krokoch.

Dekorátory sú 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, kým kód skutočne plne nepochopíte.

Dekorátory s parametrami

Zatiaľ naše dekorátory pracovali iba s dekorovanými funkciami. V Pythone je však možné vytvoriť dekorátory, ktoré prijímajú aj vlastné parametre. To nám umožňuje vytvárať flexibilnejšie dekorátory, ktorých správanie je možné prispôsobiť na základe zadaných parametrov. Tie by sme odovzdali pri aplikácii dekorátora podobne ako pri volaní bežnej funkcie:

@measure_performance(unit='ms')
def save_data_to_database(data):
    # ...

Využitie dekorátorov s parametrami je veľmi často vidieť napríklad v rôznych webových frameworkoch, ako je Django, kde je možné konfigurovať správanie funkcií na základe rôznych argumentov, napríklad zabezpečenie prístupu na základe oprávnení:

@permission_required('auth.change_user', raise_exception=True)
def edit_user(request):
    # ...

Použitím dekorátora s parametrami vytvárame v podstate „továreň na dekorátory“.

Teraz si teda vytvoríme dekorátor s parametrom, ktorý umožní odovzdať správu pred volaním ľubovoľnej funkcie:

Klikni pre editáciu
  • def add_message(message):
        def add_message_decorator(decorated_function):
            def print_message(*args, **kwargs):
                print(message)
                return decorated_function(*args, **kwargs)
    
            return print_message
    
        return add_message_decorator
    
    message = "Calling the addition function!"
    
    @add_message(message)
    def add(a, b, c):
        print(f"The result of the calculation is: {a + b + c}")
    
    message = "Calling the multiplication function!"
    
    @add_message(message)
    def multiply(a, b, c):
        print(f"The result of the calculation is: {a * b * c}")
    
    add(1, 2, 3)
    multiply(10, 20, 30)
    • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

    Pozrime sa bližšie na kód. Náš "vonkajší" dekorátor add_message() prijíma argument message a vracia skutočný dekorátor add_message_decorator(). Ten následne obaľuje naše funkcie add() a multiply(). Vďaka tomu môžeme ľahko meniť obsah správy pre rôzne funkcie, bez toho aby sme museli meniť samotný dekorátor. Vnútorná funkcia print_message() pozná hodnotu premennej message iba z takzvaného vonkajšieho kontextu, čo je ukážkou mechanizmu zvaného closure.

    Keď teda chceme vytvoriť dekorátor s parametrami, potrebujeme tri úrovne funkcií:

    • vonkajšiu funkciu, ktorá prijíma parametre dekorátora,
    • vnútornú funkciu (dekorátor), ktorá prijíma funkciu, ktorú chceme dekorovať,
    • obalenú funkciu - to je tá skutočná funkcia, ktorá rozširuje správanie pôvodnej dekorovanej funkcie a je zavolaná namiesto nej.

    Toto je práve tá "továreň na dekorátory", možnosť vytvoriť dekorátor na mieru podľa našich potrieb.

    Uzáver (closure)

    Uzáver je špeciálny typ funkcie, ktorá si "pamätá" premenné z okolia, kde bola vytvorená, a dokáže ich používať, aj keď toto okolie (čiže kontext) už neexistuje. Inými slovami, uzáver je funkcia, ktorá si "nesie so sebou" dáta , ktorá bola dostupná vo chvíli, keď vznikla:

    Klikni pre editáciu
    • def divide_number(dividend):
          def divide_by(divisor):
              return dividend / divisor
          return divide_by
      
      division_function = divide_number(10)
      print(division_function(5))
      • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

      V našom príklade sme vytvorili uzáver uložením funkcie s parametrom 10 do premennej division_function. Táto funkcia si od tejto chvíle pamätá onú hodnotu, ktorá bola nastavená pri vytvorení uzáveru, a dokáže ju použiť kedykoľvek neskôr. Pri následnom zavolaní funkcie division_function(5) sa použije uložená hodnota 10 ako delenec, ktorý sa vydelí hodnotou v parametri (5), čo vráti výsledok 2.0.

      Aj keď funkcia divide_number() (kde bol uzáver vytvorený) už skončila, hodnota premennej dividend je stále dostupná vďaka tomu, že ju Python automaticky uložil spolu s funkciou divide_by(). To je presne princíp uzáveru – funkcia si pamätá a uchováva svoj pôvodný kontext, a preto s ním môže ďalej pracovať.

      Uzávery v Pythone sú realizované prostredníctvom objektu, ktorý reprezentuje funkciu. Tento objekt obsahuje niekoľko atribútov, ktoré uchovávajú informácie o funkcii a jej kontexte. Jedným z týchto atribútov je __closure__, ktorý obsahuje referencie na voľné premenné z kontextu, kde by bola funkcia vytvorená.

      Tento mechanizmus je užitočný, pretože umožňuje vytvárať prispôsobené funkcie, ktoré si uchovávajú svoje vlastné hodnoty bez nutnosti používať globálne premenné alebo zložité štruktúry. Python tento proces automatizuje, takže uzávery fungujú jednoducho a bez zložitého nastavovania. Funkcia jednoducho získa prístup k premenným z kontextu, v ktorom bola vytvorená.

      Triedne dekorátory

      Rovnako ako sme vytvárali dekorátory pre funkcie, môžeme aj vytvoriť dekorátory pre triedy. Triedne dekorátory obvykle pridávajú, upravujú alebo rozširujú funkcionalitu triedy.

      Rovnako ako u predchádzajúcich dekorátorov, tak aj triedny dekorátor je funkciou, ktorá prijíma triedu ako argument a vracia upravenú alebo novú triedu. Pozrime sa na príklad:

      Klikni pre editáciu
      • def check_permission_level(cls):
        
            class PermissionLevel(cls):
                def __init__(self, *args, **kwargs):
                    super().__init__(*args, **kwargs)
                    self.permission_level = kwargs.get('permission_level', 0)
        
                def display_sensitive_information(self):
                    if self.permission_level >= 5:
                        return f"[Access granted]: {super().display_sensitive_information()}"
                    else:
                        return f"[Access denied]: Insufficient permission level (level {self.permission_level}/5)."
        
            return PermissionLevel
        
        @check_permission_level
        class Employee:
            def __init__(self, name, position, permission_level=0):
                self.name = name
                self.position = position
                self.permission_level = permission_level
        
            def display_information(self):
                return f"Employee: {self.name}, Position: {self.position}"
        
            def display_sensitive_information(self):
                return "Sensitive informations about the employee and the company..."
        
        employee_marc = Employee("Marc Graham", "Developer", permission_level=1)
        print(f"{employee_marc.name}: {employee_marc.display_sensitive_information()}")
        
        employee_peter = Employee("Peter Nightingale", "Manager", permission_level=5)
        print(f"{employee_peter.name}: {employee_peter.display_sensitive_information()}")
        • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

        V príklade vytvárame dekorátor check_permission_level(), ktorý pracuje s triedou namiesto funkcie. Dekorátor prepisuje konštruktor triedy a pridáva logiku na kontrolu oprávnenia. Konkrétne upravuje metódu display_sensitive_information(), aby pri nedostatočnej úrovni oprávnenia zobrazila chybovú hlášku namiesto pôvodného výstupu. Dekorátor používame nad deklaráciou triedy pomocou zápisu @check_permission_level. V prípade, že máme zamestnancov s úrovňou oprávnenia menšou ako 5, pri zavolaní jeho metódy display_sensitive_information() teraz dostávame správu o nedostatočnej miere oprávnenia.

        Metóda get() kolekcie slovník

        Kód obsahuje doposiaľ neprebranú látku, s ktorou sa stretneme až v kurze Kolekcie v Pythone. Ide o riadok:

        self.permission_level = kwargs.get('permission_level', 0)

        Tento kód využíva takzvaný slovník a jeho metódu get() na získanie hodnoty kľúča permission_level z kwargs. Metóda súčasne zaisťuje, že pokiaľ v kwargs kľúč permission_level nebude špecifikovaný, nastaví sa jeho hodnota na 0.

        Použitie triednych dekorátorov

        Výhodami triednych dekorátorov sú:

        • modularita - oddelíme rôzne funkcionality do rôznych dekorátorov a aplikujeme ich podľa potreby,
        • opakovaná použiteľnosť - raz vytvorený dekorátor je možné použiť na viac triedach,
        • rozšíriteľnosť - ľahko rozšírime funkcie existujúcich tried bez úpravy pôvodného kódu.

        Pozor si musíme dať na:

        • komplexitu - rovnako ako pri funkčných dekorátoroch je dôležité nepreháňať to s príliš mnohými funkciami v jednom dekorátore. Výsledkom budú zmätky a komplikácie pri čítaní kódu,
        • dedičnosť - dekorátor samozrejme interaguje s dedičnosťou. Ak trieda dedí z inej triedy, dekorátor môže výrazne ovplyvniť správanie potomka.

        Vytváranie triednych dekorátorov je už naozaj veľmi pokročilá technika (aj samotné funkčné dekorátory nie sú úplne triviálne), ale je neoceniteľná v určitých situáciách, keď potrebujeme meniť správanie tried dynamicky a modulárne.

        Vstavané dekorátory

        Python ponúka niekoľko vstavaných dekorátorov, ktoré umožňujú rýchlo a efektívne rozšíriť funkcionalitu vašich tried a funkcií. V kurze už sme sa zoznámili s dekorátormi @staticmethod a @classmethod. S dekorátorom @property sa zoznámime v lekcii Vlastnosti v Pythone. Vstavané dekorátory v Pythone uľahčujú radu bežných programátorských úloh a umožňujú efektívnu a elegantnú implementáciu funkcionalít. Je naozaj dôležité sa s nimi dobre zoznámiť, pretože ich budeme často stretávať v praxi.

        Vytváranie vlastných dekorátorov

        Už sme si ukázali, ako dekorátory fungujú, a videli sme, ako dokážu meniť správanie funkcií, bez toho aby sme ich (tie funkcie) museli priamo upravovať. Pretože ide o pomerne náročnú tému, celú lekciu si teraz zhrnieme a pozrieme sa, ako vytvoriť vlastný dekorátor od základu.

        Návrh dekorátora

        Základným krokom pri vytváraní dekorátora je napísať funkciu (dekorátor), ktorý prijíma funkciu ako argument a vracia inú funkciu:

        def measure_function_runtime(decorated_function):
            def measure_time():
                # some code before calling the original function
                decorated_function()
                # some code after calling the original function
            return measure_time

        Tento dekorátor measure_function_runtime() definuje vnorenú funkciu measure_time(), ktorá zavolá odovzdanú funkciu decorated_function(). Dekorátor vracia funkciu measure_time(), ktorá by mohla byť použitá na sledovanie alebo meranie behu funkcie, ak by bola doplnená o časové meranie (napríklad pomocou modulu time).

        Parametrizácia dekorátora

        Ako sme si ukázali, dekorátor vie prijímať parametre, kód upravíme:

        def set_number_of_measurements(number_of_measurements=1):
            def measure_function_runtime(decorated_function):
                def measure_time():
                    # ...
                    decorated_function()
                    # ...
                return measure_time
            return measure_function_runtime

        Tento kód obaľuje predchádzajúcu ukážku ďalšou vrstvou - funkciou set_number_of_measurements(). Tá prijíma v parametri počet meraní a vracia dekorátor measure_function_runtime(). Ten obaľuje odovzdanú funkciu decorated_function() do vnorenej funkcie measure_time().

        Použitie dekorátora

        Do tela dekorátora doplníme implementáciu merania behu funkcie a dekorátor aplikujeme na funkciu add() pomocou @ syntaxe:

        Klikni pre editáciu
        • import time
          
          def set_number_of_measurements(number_of_measurements=1):
              def measure_function_runtime(decorated_function):
                  def measure_time(*args, **kwargs):
                      runtimes = []
          
                      print(f"Starting to measure the runtime of the function {decorated_function.__name__}.")
          
                      for i in range(number_of_measurements):
                          start = time.time()
                          decorated_function(*args, **kwargs)
                          end = time.time()
                          runtime = end - start
                          runtimes.append(runtime)
          
                          print(f"Measurement {i + 1}/{number_of_measurements}: Runtime = {runtime:.6f} s.")
          
                      average_runtime = sum(runtimes) / number_of_measurements
                      print(f"Average runtime of the function {decorated_function.__name__} after {number_of_measurements} measurements: {average_runtime:.6f} s.")
          
                  return measure_time
              return measure_function_runtime
          
          @set_number_of_measurements(2)
          def add(a=10, b=20):
              print(f"{a} + {b} is {a + b}")
              time.sleep(0.225)  # Simulating a longer function runtime.
          
          add()
          • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

          Volanie dekorovanej funkcie add() je možné nahradiť zápisom set_number_of_measurements(2)(add)(). Keď každý náš vytvorený dekorátor dokážeme zapísať aj týmto spôsobom, je to dobrá známka toho, že problematike rozumieme

          Dekorátory sú silným nástrojom, ak sú používané správne. Umožňujú nám dodať dodatočné správanie funkciám alebo triedam v modulárnej a čitateľnej forme. Je ale veľmi dôležité dbať na to, aby kód zostal čitateľný a nesnažiť sa napchať za každú cenu príliš veľa funkcionality do jedného dekorátora.

          V budúcej lekcii, Vlastnosti v Pythone, sa budeme zaoberať vlastnosťami čiže gettery a settery, ktoré umožnia jednoduchšie nastavovanie a validáciu hodnôt atribútov.


           

          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
          Dekorátory v Pythone
          Všetky články v sekcii
          Objektovo orientované programovanie v Pythone
          Preskočiť článok
          (neodporúčame)
          Vlastnosti v Pythone
          Článok pre vás napísal Karel Zaoral
          Avatar
          Užívateľské hodnotenie:
          12 hlasov
          Karel Zaoral
          Aktivity