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