Lekce 16 - Dekorátory v Pythonu
V předešlém cvičení, Řešené úlohy k 13.-14. lekci Python, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
V následujícím tutoriálu objektově orientovaného programování v Pythonu se podrobněji podíváme na dekorátory. Vysvětlíme si, že dekorátory jsou nástroj, který nám umožňuje snadno přidat nové chování funkcím, metodám nebo třídám bez nutnosti měnit jejich původní kód.
Dekorátory v Pythonu
Představme si, že pracujeme na projektu, kde máme v kódu velké množství metod. Najednou zjistíme, že všechny tyto metody potřebují vykonávat něco navíc – například přidat ke svému výstupu čas jejich volání. Můžeme začít přepisovat jednu metodu po druhé a ručně tuto funkcionalitu doplnit. To by ale bylo velmi zdlouhavé a náchylné k chybám. Místo toho můžeme využít dekorátory, které tento problém vyřeší elegantně a bez nutnosti měnit původní kód. V praxi se dekorátory často používají například pro logování, autorizace nebo validaci vstupů.
Základní syntaxe dekorátorů
Základní principy si vyzkoušíme na jednoduché funkci
zapis_do_deniku()
, kterou následně odekorujeme:
def zapis_do_deniku(zprava): print(zprava)
Naše funkce momentálně pouze vypisuje zprávu, kterou jí předáme. Co ale udělat, pokud chceme ke každé zprávě automaticky přidat datum a čas? Místo toho, abychom upravili původní kód, použijeme dekorátor. Ten nám umožní změnu provést rychle a elegantně, aniž bychom zasahovali do původní funkce. Dekorátor pak můžeme využít i u dalších funkcí, které něco vypisují a připojit tak i k jejich výpisům časové razítko:
from datetime import datetime
def pridej_casove_razitko(dekorovana_funkce):
def uprav_zpravu(zprava):
cas = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return dekorovana_funkce("[" + cas + "] " + zprava)
return uprav_zpravu
@pridej_casove_razitko
def zapis_do_deniku(zprava):
print(zprava)
zapis_do_deniku("Přihlásit se na školení ITnetwork")
Vytvořili jsme funkci pridej_casove_razitko()
, kterou
používáme jako dekorátor. Dekorátor totiž v téhle
podobě není nic jiného než obyčejná funkce, která v parametru
přijme jinou funkci, upraví její chování a následně jí vrátí.
Fungování je zajištěno pomocí vnořené funkce uprav_zpravu()
uvnitř dekorátoru, která obaluje tu původní tak, že při jejím zavolání
přidává k původní zprávě ještě aktuální čas. Po vytvoření našeho
dekorátoru "ozdobíme" původní funkci přidáním znaku
@
a názvu dekorátoru nad deklaraci upravované funkce. V
tuto chvíli při zavolání zapis_do_deniku()
dostaneme vždy už
odekorovaný výstup, tedy řetězec s aktuálním časem.
Funkce jako objekty první třídy
Nejspíše nás překvapil způsob, jak s funkcemi pracujeme. Najednou je píšeme bez závorek a předáváme je jako parametry dalším funkcím! Toto je možné díky tomu, že funkce jsou v Pythonu takzvané objekty první třídy. Znamená to, že s nimi můžeme zacházet stejně jako s klasickými objekty. Můžeme je tedy předávat jako parametr jiným funkcím a můžeme je také uložit do proměnných. Když tímto způsobem pracujeme s funkcí, píšeme její název bez závorek – nejde totiž o její volání, ale o předávání funkce jako takové. Podívejme se na jednoduchý příklad:
def zapis_data(): return "Zapisuji data do databáze." print(zapis_data()) print(zapis_data)
Máme jednoduchou funkci, která vrací textový řetězec. V případě, že
zavoláme print(zapis_data())
, vypíše se nám do konzole
řetězec vrácený touto funkcí - funkci jsme tedy zavolali. Když ale budeme
chtít vytisknout funkci samotnou, tedy bez závorek, dostaneme jen informaci o
objektu reprezentujícím tuto funkci, konkrétně její paměťovou adresu:
Konzolová aplikace
Zapisuji data do databáze.
function zapis_data at 0x0000020307973E20
Dekorování funkcí s více parametry
V úvodním příkladu dekorátor pracoval s funkcí, která přijímala
pouze jeden parametr, zprávu k výpisu. Ukažme si, jak
vytvořit dekorátor, který dokáže pracovat s funkcemi, jež mají
různý počet parametrů. V takových případech použijeme
parametry *args
a **kwargs
.
Parametr *args
uchovává kolekci všech
pozičních argumentů, zatímco **kwargs
obsahuje
kolekci všech klíčových argumentů. Pomocí těchto
konstrukcí můžeme k předávaným argumentům přistupovat bez ohledu na
jejich počet či typ. Tento způsob práce s parametry zajišťuje
dekorátorům velkou flexibilitu.
Podívejme se na příklad funkce pro výpočet celkové ceny objednávky. Pomocí dekorátoru zajistíme, že k základní ceně bude automaticky přidána daň:
def pridej_dan(dekorovana_funkce):
def pridej_dan_k_vysledku(*args, **kwargs):
cena = dekorovana_funkce(*args, **kwargs)
cena_s_dani = cena + (args[0] * args[1]) * 0.21
return f"Celková cena objednávky s daní je: {cena_s_dani:.2f} Kč"
return pridej_dan_k_vysledku
@pridej_dan
def zaznamenej_objednavku(cena_za_kus, mnozstvi, doprava=0, sleva=0):
return cena_za_kus * mnozstvi + doprava - sleva
print(zaznamenej_objednavku(200, 3, doprava=50, sleva=100))
Díky *args
a **kwargs
je náš dekorátor
flexibilní a dokáže dekorovat jakoukoli funkci bez
ohledu na počet a typ jejích argumentů. V našem kódu jsme
postupovali stejně jako v předchozím příkladu. Opět jsme v dekorátoru
vytvořili vnitřní funkci pridej_dan_k_vysledku()
, ve které jsme
k původní ceně objednávky přidali i daň. K pozičním argumentům
*args
jsme přistoupili přes jejich index v hranatých závorkách
(args[0]
v našem případě pro cena_za_kus
). To nám
umožnilo vzít pouze dva parametry (cena_za_kus
a
mnozstvi
) a z nich vypočíst 21% daň, kterou jsme následně
přičetli k celkové ceně.
Pro přístup ke klíčovým argumentům uloženým v **kwargs
bychom do hranatých závorek uvedli název klíče, například
kwargs["sleva"]
, pokud bychom chtěli získat hodnotu parametru
sleva
.
Návratovými hodnotami *args
a
**kwargs
jsou tuple a slovník. Tyto datové typy ještě neznáme,
seznámíme se s nimi až v kurzu Kolekce v
Pythonu. Obě návratové hodnoty si ale kdykoliv snadno převedeme na
seznam. Seznam pozičních argumentů nám vrátí
volání list(args)
a seznam dvojic klíčových argumentů
(název argumentu, hodnota) volání list(kwargs.items())
.
Vícenásobné dekorování
Jedné funkci můžeme přiřadit více dekorátorů. Mluvíme pak o vícenásobném dekorování. Dekorátory se aplikují ve vrstvách, přičemž nejprve se aplikuje dekorátor, který je nejblíže funkci. Nakonec se aplikuje ten, který je v kódu nejvýše. Pro zjednodušení si můžeme představit každý dekorátor jako obal, do kterého postupně balíme naši funkci.
Na ukázku přidáme k předcházejícímu příkladu ještě náš dekorátor s časovým razítkem, který upravíme, aby byl univerzálnější:
from datetime import datetime
def pridej_dan(dekorovana_funkce):
def pridej_dan_k_vysledku(*args, **kwargs):
cena = dekorovana_funkce(*args, **kwargs)
cena_s_dani = cena + (args[0] * args[1]) * 0.21
return f"Celková cena objednávky s daní je: {cena_s_dani:.2f} Kč"
return pridej_dan_k_vysledku
def pridej_casove_razitko(dekorovana_funkce):
def uprav_zpravu(*args, **kwargs):
cas = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return "[" + cas + "] " + str(dekorovana_funkce(*args, **kwargs))
return uprav_zpravu
@pridej_casove_razitko
@pridej_dan
def zaznamenej_objednavku(cena_za_kus, mnozstvi, doprava=0, sleva=0):
return cena_za_kus * mnozstvi + doprava - sleva
print(zaznamenej_objednavku(200, 3, doprava=50, sleva=100))
V případě, že zavoláme funkci zaznamenej_objednavku()
,
Python nejprve obalí původní funkci dekorátorem pridej_dan()
.
Ten vrátí novou funkci, která po zavolání vrátí řetězec obsahující
cenu s připočítanou daní. Poté je tato nová funkce obalena dekorátorem
pridej_casove_razitko()
, který upravuje její chování tak, že
před řetězec s cenou a daní přidá aktuální čas ve formě časového
razítka.
V příští lekci, Dekorátory podruhé - Parametrické a třídní dekorátory, budeme v tématu dekorátorů pokračovat.