NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
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 17 - Dekorátory podruhé - Parametrické a třídní dekorátory

V předchozí lekci, Dekorátory v Pythonu, jsme se seznámili s dekorátory a vysvětlili si princip jejich použití.

V dnešním tutoriálu objektově orientovaného programování v Pythonu budeme pokračovat v práci s dekorátory. Naučíme se je parametrizovat a aplikovat na třídu. V závěru lekce si téma shrneme a ukážeme si celý postup vytvoření dekorátoru v navazujících krocích.

Dekorátory jsou pokročilé 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.

Dekorátory s parametry

Zatím naše dekorátory pracovaly pouze s dekorovanými funkcemi. V Pythonu je však možné vytvořit dekorátory, které přijímají i vlastní parametry. To nám umožňuje vytvářet flexibilnější dekorátory, jejichž chování lze přizpůsobit na základě zadaných parametrů. Ty bychom předali při aplikaci dekorátoru podobně jako při volání běžné funkce:

@zmer_vykon(jednotka='ms')
def uloz_data_do_databaze(data):
    # ...

Využití dekorátorů s parametry je velmi často vidět například v různých webových frameworcích, jako je Django, kde lze konfigurovat chování funkcí na základě různých argumentů, například zabezpečení přístupu na základě oprávnění:

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

Použitím dekorátoru s parametry vytváříme v podstatě "továrnu na dekorátory".

Nyní si tedy vytvoříme dekorátor s parametrem, který umožní předat zprávu před voláním libovolné funkce:

def pridej_zpravu(zprava):
    def pridej_zpravu_dekorator(dekorovana_funkce):
        def vypis_zpravu(*args, **kwargs):
            print(zprava)
            return dekorovana_funkce(*args, **kwargs)

        return vypis_zpravu

    return pridej_zpravu_dekorator

zprava = "Volám funkci pro sčítání!"

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

zprava = "Volám funkci pro násobení!"

@pridej_zpravu(zprava)
def vynasob(a, b, c):
    print(f"Výsledek výpočtu je: {a * b * c}")

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

Podívejme se blíže na kód. Náš "vnější" dekorátor pridej_zpravu() přijímá argument zprava a vrací skutečný dekorátor pridej_zpravu_dekorator(). Ten následně obaluje naše funkce secti() a vynasob(). Díky tomu můžeme snadno měnit obsah zprávy pro různé funkce, aniž bychom museli měnit samotný dekorátor. Vnitřní funkce vypis_zpravu() zná hodnotu proměnné zprava pouze z takzvaného vnějšího kontextu, což je ukázkou mechanismu zvaného closure. Způsob jeho fungování si vysvětlíme za chvíli.

Když tedy chceme vytvořit dekorátor s parametry, potřebujeme tři úrovně funkcí:

  • vnější funkci, která přijímá parametry dekorátoru,
  • vnitřní funkci (dekorátor), která přijímá funkci, již chceme dekorovat,
  • obalenou funkci - to je ta skutečná funkce, která rozšiřuje chování původní dekorované funkce a je zavolána místo ní.

Toto je zmíněná "továrna na dekorátory", tedy možnost vytvořit dekorátor na míru podle našich potřeb.

Uzávěr (closure)

Uzávěr je speciální typ funkce, která si "pamatuje" proměnné z okolí, kde byla vytvořena, a dokáže je používat, i když toto okolí (neboli kontext) už neexistuje. Jinými slovy, uzávěr je funkce, která si "nese s sebou" data, která byla dostupná ve chvíli, kdy vznikla:

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

delici_funkce = vydel_cislo(10)
print(delici_funkce(5))

V našem příkladu jsme vytvořili uzávěr uložením funkce s parametrem 10 do proměnné delici_funkce. Tato funkce si od této chvíle pamatuje onu hodnotu, která byla nastavena při vytvoření uzávěru, a dokáže ji použít kdykoli později. Při následném zavolání funkce delici_funkce(5) se použije uložená hodnota 10 jako delenec, který se vydělí hodnotou v parametru (5), což vrátí výsledek 2.0.

I když funkce vydel_cislo() (kde byl uzávěr vytvořen) už skončila, hodnota proměnné delenec je stále dostupná díky tomu, že ji Python automaticky uložil spolu s funkcí vydel_cislem(). To je přesně princip uzávěru – funkce si pamatuje a uchovává svůj původní kontext, a proto s ním může dál pracovat.

Uzávěry v Pythonu jsou realizovány prostřednictvím objektu, který reprezentuje funkci. Tento objekt obsahuje několik atributů, které uchovávají informace o funkci a jejím kontextu. Jedním z těchto atributů je __closure__, který obsahuje reference na volné proměnné z kontextu, kde by byla funkce vytvořena.

Tento mechanismus je užitečný, protože umožňuje vytvářet přizpůsobené funkce, které si uchovávají své vlastní hodnoty bez nutnosti používat globální proměnné nebo složité struktury. Python tento proces automatizuje, takže uzávěry fungují snadno a bez složitého nastavování. Funkce jednoduše získá přístup k proměnným z kontextu, ve kterém byla vytvořena.

Třídní dekorátory

Stejně jako jsme vytvářeli dekorátory pro funkce, můžeme také vytvořit dekorátory pro třídy. Třídní dekorátory obvykle přidávají, upravují nebo rozšiřují funkcionalitu třídy.

Stejně jako u předchozích dekorátorů, tak i třídní dekorátor je funkcí, která přijímá třídu jako argument a vrací upravenou nebo novou třídu. Podívejme se na příklad:

def over_uroven_opravneni(trida):

    class UrovenOpravneni(trida):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.uroven_opravneni = kwargs.get('uroven_opravneni', 0)

        def zobraz_citlive_informace(self):
            if self.uroven_opravneni >= 5:
                return f"[Přístup povolen]: {super().zobraz_citlive_informace()}"
            else:
                return f"[Přístup odepřen]: Nedostatečná úroveň oprávnění (úroveň {self.uroven_opravneni}/5)."

    return UrovenOpravneni

@over_uroven_opravneni
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"Zaměstnanec: {self.jmeno}, Pozice: {self.pozice}"

    def zobraz_citlive_informace(self):
        return "Citlivé informace o zaměstnanci a firmě..."

zamestnanec_jan = Zamestnanec("Jan Novák", "Vývojář", uroven_opravneni=1)
print(f"{zamestnanec_jan.jmeno}: {zamestnanec_jan.zobraz_citlive_informace()}")

zamestnanec_petr = Zamestnanec("Petr Sýkora", "Manažer", uroven_opravneni=5)
print(f"{zamestnanec_petr.jmeno}: {zamestnanec_petr.zobraz_citlive_informace()}")

V příkladu vytváříme dekorátor over_uroven_opravneni(), který pracuje s třídou namísto funkce. Dekorátor přepisuje konstruktor třídy a přidává logiku pro kontrolu oprávnění. Konkrétně upravuje metodu zobraz_citlive_informace(), aby při nedostatečné úrovni oprávnění zobrazila chybovou hlášku místo původního výstupu. Dekorátor používáme nad deklarací třídy pomocí zápisu @over_uroven_opravneni. V případě, že máme zaměstnance s úrovní oprávnění menší než 5, při zavolání jeho metody zobraz_citlive_informace() nyní dostáváme zprávu o nedostatečné míře oprávnění.

Metoda get() kolekce slovník

Kód obsahuje doposud neprobranou látku, se kterou se setkáme až v kurzu Kolekce v Pythonu. Jedná se o řádek:

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

Tento kód využívá takzvaný slovník a jeho metodu get() pro získání hodnoty klíče uroven_opravneni z kwargs. Metoda současně zajišťuje, že pokud v kwargs klíč uroven_opravneni nebude specifikovaný, nastaví se jeho hodnota na 0.

Použití třídních dekorátorů

Výhodou použití třídních dekorátorů je:

  • modularita - oddělíme různé funkcionality do různých dekorátorů a aplikujeme je podle potřeby,
  • opakovaná použitelnost - jednou vytvořený dekorátor lze použit na více třídách,
  • rozšiřitelnost - snadno rozšíříme funkce existujících tříd bez úpravy původního kódu.

Pozor si musíme dát na:

  • komplexitu - stejně jako s funkčními dekorátory je důležité nepřeplácat dekorátory příliš mnoha funkcemi. Výsledkem bude zmatení a komplikace při čtení kódu,
  • dědičnost - dekorátor samozřejmě interaguje s dědičností. Pokud třída dědí z jiné třídy, dekorátor může výrazně ovlivnit chování potomka.

Vytváření třídních dekorátorů je už opravdu hodně pokročilá technika (i samotné funkční dekorátory nejsou úplně triviální), ale je velice užitečná v situacích, kdy potřebujeme měnit chování tříd dynamicky a modulárně.

Vestavěné dekorátory

Python nabízí několik vestavěných dekorátorů, které umožňují rychle a efektivně rozšířit funkčnost vašich tříd a funkcí. V kurzu už jsme se seznámili s dekorátory @staticmethod a @classmethod. S dekorátorem @property se seznámíme v lekci Vlastnosti v Pythonu. Vestavěné dekorátory v Pythonu usnadňují řadu běžných programátorských úkolů a umožňují efektivní a elegantní implementaci funkcionalit. Je opravdu důležité se s nimi dobře seznámit, protože je budeme často potkávat v praxi.

Vytváření vlastních dekorátorů

Již jsme si ukázali, jak dekorátory fungují. Viděli jsme, jak dokáží měnit chování funkcí či tříd, aniž bychom je museli přímo upravovat. Protože jde o poměrně náročné téma, celou lekci si teď shrneme a podíváme se, jak vytvořit vlastní dekorátor od základu.

Návrh dekorátoru

Základním krokem při vytváření dekorátoru je napsat funkci (tj. dekorátor), který přijímá funkci jako argument a vrací jinou funkci:

def zjisti_dobu_behu_funkce(dekorovana_funkce):
    def zmer_cas():
        # nějaký kód před voláním původní funkce
        dekorovana_funkce()
        # nějaký kód po volání původní funkce
    return zmer_cas

Tento dekorátor zjisti_dobu_behu_funkce() definuje vnořenou funkci zmer_cas(), která zavolá předanou funkci dekorovana_funkce(). Dekorátor vrací funkci zmer_cas(), která by mohla být použita ke sledování nebo měření běhu funkce, pokud by byla doplněna o časové měření (například pomocí modulu time).

Parametrizace dekorátoru

Jak jsme si ukázali, dekorátor umí přijímat parametry:

def nastav_pocet_mereni(pocet_mereni=1):
    def zjisti_dobu_behu_funkce(dekorovana_funkce):
        def zmer_cas():
            # ...
            dekorovana_funkce()
            # ...
        return zmer_cas
    return zjisti_dobu_behu_funkce

Tento kód obaluje předchozí další vrstvou - funkcí nastav_pocet_mereni(). Ta přijímá v parametru počet měření a vrací dekorátor zjisti_dobu_behu_funkce(). Ten obaluje předanou funkci dekorovana_funkce() do vnořené funkce zmer_cas().

Použití dekorátoru

Do těla dekorátoru doplníme implementaci měření běhu funkce a dekorátor aplikujeme na funkci scitej() pomocí @ syntaxe:

import time

def nastav_pocet_mereni(pocet_mereni=1):
    def zjisti_dobu_behu_funkce(dekorovana_funkce):
        def zmer_cas(*args, **kwargs):
            doby_behu = []

            print(f"Začínám měřit dobu běhu funkce {dekorovana_funkce.__name__}.")

            for i in range(pocet_mereni):
                zacatek = time.time()
                dekorovana_funkce(*args, **kwargs)
                konec = time.time()
                cas_behu = konec - zacatek
                doby_behu.append(cas_behu)

                print(f"Měření {i + 1}/{pocet_mereni}: Doba běhu = {cas_behu:.6f} s.")

            prumerna_doba = sum(doby_behu) / pocet_mereni
            print(f"Průměrná doba běhu funkce {dekorovana_funkce.__name__} po {pocet_mereni} měřeních: {prumerna_doba:.6f} s.")

        return zmer_cas
    return zjisti_dobu_behu_funkce

@nastav_pocet_mereni(2)
def scitej(a=10, b=20):
    print(f"{a} + {b} je {a + b}")
    time.sleep(0.225)  # Simulace delšího běhu funkce.

scitej()

Volání dekorované funkce scitej() lze nahradit zápisem nastav_pocet_mereni(2)(scitej)(). Když každý náš vytvořený dekorátor dokážeme zapsat i tímto způsobem, je to dobrá známka toho, že problematice rozumíme.

Dekorátory jsou silným nástrojem, pokud jsou používány správně. Umožňují nám dodat dodatečné chování funkcím nebo třídám v modulární a čitelné formě. Je ale velmi důležité dbát na to, aby kód zůstal čitelný a nesnažit se napěchovat za každou cenu příliš mnoho funkčnosti do jednoho dekorátoru.

V příští lekci, Vlastnosti v Pythonu, se budeme zabývat vlastnostmi neboli gettery a settery, které umožní snazší nastavování a validaci hodnot atributů.


 

Předchozí článek
Dekorátory v Pythonu
Všechny články v sekci
Objektově orientované programování v Pythonu
Přeskočit článek
(nedoporučujeme)
Vlastnosti v Pythonu
Článek pro vás napsal Karel Zaoral
Avatar
Uživatelské hodnocení:
291 hlasů
Karel Zaoral
Aktivity