Mikuláš je tu! Získaj 90 % extra kreditov ZADARMO s promo kódom CERTIK90 pri nákupe od 1 199 kreditov. Len do nedele 7. 12. 2025! Zisti viac:
NOVINKA: Najžiadanejšie rekvalifikačné kurzy teraz s 50% zľavou + kurz AI ZADARMO. Nečakaj, táto ponuka dlho nevydrží! Zisti viac:

18. 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 jednotlivých 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:

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)

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 "so sebou nesie" dáta , a bola dostupná vo chvíli, keď vznikla:

def divide_number(dividend):
    def divide_by(divisor):
        return dividend / divisor
    return divide_by

division_function = divide_number(10)
print(division_function(5))

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á 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 vytvoriť aj 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:

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()}")

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 sme sa už 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:

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()

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ý znak toho, že problematike rozumieme

Dekorátory sú silným nástrojom, ak sú používané správne. Umožňujú nám pridať 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 nasledujúcej lekcii, Vlastnosti v Pythone, sa budeme zaoberať vlastnosťami čiže gettermi a settermi, ktoré umožnia jednoduchšie nastavovanie a validáciu hodnôt atribútov.


 

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:
108 hlasov
Karel Zaoral
Aktivity