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


 

Předchozí článek
Řešené úlohy k 13.-14. lekci Python
Všechny články v sekci
Objektově orientované programování v Pythonu
Přeskočit článek
(nedoporučujeme)
Dekorátory podruhé - Parametrické a třídní dekorátory
Článek pro vás napsal Karel Zaoral
Avatar
Uživatelské hodnocení:
300 hlasů
Karel Zaoral
Aktivity