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