Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
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 náročné 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ímco dosud naše dekorátory přijímaly jako argumenty jenom funkce, Python umí vytvořit také dekorátory, které samy o sobě přijímají parametry. Díky tomu dokážeme jednoduše vytvářet dekorátory, které se chovají různě na základě poskytnutých parametrů.

Využití dekorátorů s parametry je velmi často vidět například v různých webových frameworcích, kde je možné konfigurovat, jak se mají funkce (např. zpracování HTTP požadavků) chovat na základě různých argumentů.

Když 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, kterou 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í.

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

def zprava_dekorator(zprava):
    def vnitrni_dekorator(func):
        def obalena_funkce(*args, **kwargs):
            print(zprava)
            return func(*args, **kwargs)

        return obalena_funkce

    return vnitrni_dekorator

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

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

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

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

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

Toto je právě ta "továrna na dekorátory" - možnost vytvořit dekorátor na míru podle našich potřeb.

Podívejme se blíže na kód. Náš "vnější" dekorátor zprava_dekorator() přijímá argument zprava a vrací skutečný dekorátor vnitrni_dekorator(). Ten následně obaluje naše funkce secti() a nasob(). 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 obalena_funkce() zná hodnotu proměnné zprava pouze z takzvaného vnějšího kontextu, což je ukázkou mechanismu zvaného closure.

Uzávěr (closure)

Uzávěr (closure) je funkce, která si "pamatuje" své volné proměnné z okolního kontextu, ve kterém byla definována, a dokáže k nim přistupovat i po skončení tohoto kontextu. Jednoduše řečeno, closure je funkce společně s nějakým zachyceným kontextem. V kódu je tento "kontext" tvořen proměnnými, které jsou dostupné v okamžiku vytvoření closure:

def vnejsi_funkce(delenec):
    def vnitrni_funkce(delitel):
        return delenec / delitel
    return vnitrni_funkce

moje_funkce = vnejsi_funkce(10)
print(moje_funkce(5))  # Výstup: 2

V tomto příkladu je vnitrni_funkce() uzávěrem, který má přístup k proměnné delenec i po tom, co vnejsi_funkce() skončila.

Vysvětlení, proč si funkce "pamatuje" svůj kontext, je spojeno s tím, jak Python funguje "pod kapotou". 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 byla funkce vytvořena. Když definujeme vnořenou funkci uvnitř jiné funkce a tato vnořená funkce odkazuje na proměnné z vnější funkce, Python closure vytvoří automaticky.

Třídní dekorátory

Stejně jako jsme vytvářeli dekorátory pro funkce, budeme také tvoř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 funkční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 tridni_dekorator(trida):
    class RozsirenaTrida(trida):
        def nova_metoda(self):
            return "Já jsem nová metoda třídy MojeTrida přidaná pomocí třídního dekorátoru"
    return RozsirenaTrida

@tridni_dekorator
class MojeTrida:
    def metoda(self):
        return "Já jsem původní metoda třídy MojeTrida"

instance = MojeTrida()
print(instance.metoda())
print(instance.nova_metoda())

Výhodami třídních dekorátorů jsou:

  • 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 budou 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 k nezaplacení v určitých 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í, a viděli jsme, jak dokáží měnit chování funkcí, aniž bychom je (ty funkce) 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 muj_dekorator(funkce):
    def obalena_funkce():
        # nějaký kód před voláním původní funkce
        funkce()
        # nějaký kód po volání původní funkce
    return obalena_funkce

Parametrizace dekorátoru

Jak jsme si ukázali, dekorátor umí přijímat parametry. K tomu potřebujeme další vnější funkci, která obklopí náš dekorátor:

def dekorator_s_parametry(parametr):
    def muj_dekorator(funkce):
        def obalena_funkce():
            if parametr:
                # kód před funkci
                funkce()
                # kód po funkci
        return obalena_funkce
    return muj_dekorator

Použití dekorátoru

K aplikaci dekorátoru na funkci použijeme @ syntaxi:

def dekorator_s_parametry(parametr):
    def muj_dekorator(funkce):
        def obalena_funkce():
            if parametr == "pozdrav":
                print("Mám tě pozdravit.")
                funkce()
                print("Tak jsem tě pozdravil.")
        return obalena_funkce
    return muj_dekorator

@dekorator_s_parametry("pozdrav")
def pozdrav():
    print("Ahoj!")

pozdrav()

Volání pozdrav() lze bez použití @dekorator_s_parametry("pozdrav") nahradit zápisem dekorator_s_parametry("pozdrav")(pozdrav)(). Když každý náš vytvořený dekorátor dokážeme zapsat i tímto způsobem, je to dobrá známka toho, že problematice dobře 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í:
102 hlasů
Karel Zaoral
Aktivity