IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

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ľ čo doteraz naše dekorátory prijímali ako argumenty len funkcie, Python vie vytvoriť aj dekorátory, ktoré sami o sebe prijímajú parametre. Vďaka tomu dokážeme jednoducho vytvárať dekorátory, ktoré sa správajú rôzne na základe poskytnutých parametrov.

Využitie dekorátorov s parametrami je veľmi často vidieť napríklad v rôznych webových frameworkoch, kde je možné konfigurovať, ako sa majú funkcie (napr. spracovanie HTTP požiadaviek) správať na základe rôznych argumentov.

Použitím dekorátora s parametrami vytvárame v podstate "továreň na dekorátory":

def zprava_dekorator(zprava):
    def pridej_zpravu(func):
        def vypis_zpravu(*args, **kwargs):
            print(zprava)
            return func(*args, **kwargs)

        return vypis_zpravu

    return pridej_zpravu

zprava = "Volám funkciu pre sčítanie!"

@zprava_dekorator(zprava)
def secti(a, b, c):
    print(f"Výsledok výpočtu je: {a + b + c}")

zprava = "Volám funkciu pre násobenie!"

@zprava_dekorator(zprava)
def nasob(a, b, c):
    print(f"Výsledok výpočtu je: {a * b * c}")

secti(1, 2, 3)
nasob(10, 20, 30)

Pozrime sa bližšie na kód. Náš "vonkajší" dekorátor zprava_dekorator() prijíma argument zprava a vracia skutočný dekorátor pridej_zpravu(). Ten následne obaľuje naše funkcie secti() a nasob(). 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 vypis_zpravu() pozná hodnotu premennej zprava 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 (closure) je funkcia, ktorá si "pamätá" svoje voľné premenné z okolitých kontextov, v ktorých bola definovaná, a dokáže k nim pristupovať aj po skončení tohto kontextu. Jednoducho povedané, closure je funkcia spolu s nejakým zachyteným kontextom. V kóde je tento "kontext" tvorený premennými, ktoré sú dostupné v okamihu vytvorenia closure:

def vydel_cislo(delenec):
    def deleni(delitel):
        return delenec / delitel
    return deleni

delici_funkce = vydel_cislo(10)
print(delici_funkce(5))  # Výstup: 2

V tomto príklade je funkcia deleni() uzáverom, ktorý má prístup k premennej delenec aj po tom, čo funkcia vydel_cislo() skončila.

Vysvetlenie, prečo si funkcia "pamätá" svoj kontext, je spojené s tým, ako Python funguje "pod kapotou". 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 bola funkcia vytvorená. Keď definujeme vnorenú funkciu vnútri inej funkcie a táto vnorená funkcia odkazuje na premenné z vonkajšej funkcie, Python closure vytvorí automaticky.

Triedne dekorátory

Rovnako ako sme vytvárali dekorátory pre funkcie, budeme tiež tvoriť dekorátory pre triedy. Triedne dekorátory obvykle pridávajú, upravujú alebo rozširujú funkcionalitu triedy.

Rovnako ako pri funkčných dekorátoroch, 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 bezpecnostni_overeni(trida):

    class UrovenOpravneni(trida):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            # Predpokladáme, že každý zamestnanec má atribút 'uroven_opravneni'
            self.uroven_opravneni = kwargs.get('uroven_opravneni', 0)

        def zobraz_citlive_informace(self):
            if self.uroven_opravneni >= 5:  # Predpokladajme, že oprávnenie 5 je potrebné pre prístup
                return super().zobraz_citlive_informace()
            else:
                return " nemá oprávnenie pre zobrazenie citlivých informácií."

    return UrovenOpravneni

@bezpecnostni_overeni
class Zamestnanec:
    def __init__(self, jmeno, pozice, uroven_opravneni=0):
        self.jmeno = jmeno
        self.pozice = pozice
        self.uroven_opravneni = uroven_opravneni

    def zobraz_informace(self):
        return f"Zamestnanec: {self.jmeno}, Pozícia: {self.pozice}"

    def zobraz_citlive_informace(self):
        return ": citlivé informácie o zamestnancovi a firme..."

# Zamestnanec s nízkou úrovňou oprávnenia
zamestnanec_jan = Zamestnanec("Jan Novák", "Vývojár", uroven_opravneni=1)
print(zamestnanec_jan.jmeno + zamestnanec_jan.zobraz_citlive_informace())

# Zamestnanec s vysokou úrovňou oprávnenia
zamestnanec_petr = Zamestnanec("Petr Sýkora", "Manažér", uroven_opravneni=5)
print(zamestnanec_petr.jmeno + zamestnanec_petr.zobraz_citlive_informace())

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.

Kód obsahuje zaujímavú časť, ktorú budeme preberať až v kurze Kolekcie. jedná sa o riadok:

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

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

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 (t. j. dekorátor), ktorý prijíma funkciu ako argument a vracia inú funkciu:

def doba_behu_funkce(func):
    def zmer_cas():
        # kód začne merať čas pred volaním pôvodnej funkcie
        func()  # funkcia, ktorú budeme chcieť dekorovať
        # kód ukončí meranie času po volaní pôvodnej funkcie
    return zmer_cas

Parametrizácia dekorátora

Ako sme si ukázali, dekorátor vie prijímať parametre. K tomu potrebujeme ďalšiu vonkajšiu funkciu, ktorá obklopí náš dekorátor:

def mereni_behu_funkce(povoleno=true):
    def doba_behu_funkce(func):
        def zmer_cas():
            if povoleno:
                # kód pred funkciou
                func()  # funkcia, ktorú budeme chcieť dekorovať
                # kód po funkcii
        return zmer_cas
    return doba_behu_funkce

Použitie dekorátora

K aplikácii dekorátora na funkciu použijeme @ syntax:

import time

def mereni_behu_funkce(povoleno):
    def doba_behu_funkce(func):
        def zmer_cas(*args, **kwargs):
            if povoleno:
                zacatek = time.time()
                print("Začínam merať dĺžku behu funkcie scitej().")
                vysledek = func(*args, **kwargs)  # Volá pôvodnú funkciu s predanými argumentmi.
                konec = time.time()
                print("Dokončil som meranie.")
                print(f"Čas behu funkcie {func.__name__}: {konec - zacatek:.5f} sekúnd.")
                return vysledek
            else:
                print("Meranie neprebehlo.")
                return func(*args, **kwargs)  # Ak nie je povolené meranie, volá pôvodnú funkciu.
        return zmer_cas
    return doba_behu_funkce

@mereni_behu_funkce(povoleno=True)
def scitej(a=10, b=20):
    print("10 + 20 je", a + b)
    time.sleep(1)  # Simulácia dlhšieho behu funkcie.

scitej()

Volanie scitej() je možné bez použitia @mereni_behu_funkce(povoleno) nahradiť zápisom mereni_behu_funkce(povoleno=True)(scitej)(). Keď každý náš vytvorený dekorátor dokážeme zapísať aj týmto spôsobom, je to dobrá známka toho, že problematike dobre 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.


 

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:
Ešte nikto nehodnotil, buď prvý!
Karel Zaoral
Aktivity