16. diel - Dekorátory v Pythone
V predchádzajúcom cvičení, Riešené úlohy k 13.-14. lekcii OOP v Pythone, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.
V nasledujúcom tutoriáli objektovo orientovaného programovania v Pythone sa podrobnejšie pozrieme na dekorátory. Vysvetlíme si, že dekorátory sú nástroj, ktorý nám umožňuje ľahko pridať nové správanie funkciám, metódam alebo triedam bez nutnosti meniť ich pôvodný kód.
Dekorátory v Pythone
Predstavme si, že pracujeme na projekte, kde máme v kóde veľké množstvo metód. Zrazu zistíme, že všetky tieto metódy potrebujú vykonávať niečo navyše – napríklad pridať k svojmu výstupu čas ich volania. Môžeme začať prepisovať jednu metódu po druhej a ručne túto funkcionalitu doplniť. To by ale bolo veľmi zdĺhavé a náchylné k chybám. Namiesto toho môžeme využiť dekorátory, ktoré tento problém vyriešia elegantne a bez nutnosti meniť pôvodný kód. V praxi sa dekorátory často používajú napríklad na logovanie, autorizáciu alebo validáciu vstupov.
Základná syntax dekorátorov
Základné princípy si vyskúšame na jednoduchej funkcii write_to_diary(), ktorú následne odokorujeme:
def write_to_diary(message) print(message)
Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.
Naša funkcia momentálne iba vypisuje správu, ktorú jej odovzdáme. Čo ale urobiť, ak chceme ku každej správe automaticky pridať dátum a čas? Namiesto toho, aby sme upravili pôvodný kód, použijeme dekorátor. Ten nám umožní zmenu vykonať rýchlo a elegantne bez toho, aby sme zasahovali do pôvodnej funkcie. Dekorátor potom môžeme využiť aj pri ďalších funkciách, ktoré niečo vypisujú a pripojiť tak aj k ich výpisom časovú pečiatku:
from datetime import datetime def add_timestamp(decorated_function): def modify_message(message): time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") return decorated_function("[" + time + "] " + message) return modify_message @add_timestamp def write_to_diary(message): print(message) write_to_diary("Sign up for Python OOP course")
Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.
Vytvorili sme funkciu add_timestamp()
, ktorú používame ako
dekorátor. Dekorátor totiž v tejto podobe nie je nič iné
ako obyčajná funkcia, ktorá v parametri prijme inú funkciu, upraví
jej správanie a následne ju vráti. Fungovanie je zaistené pomocou
vnorenej funkcie modify_message()
vo vnútri dekorátora, ktorá
obaľuje tú pôvodnú tak, že pri jej zavolaní pridáva k pôvodnej správe
ešte aktuálny čas. Po vytvorení nášho dekorátora "ozdobíme" pôvodnú
funkciu pridaním znaku @
a názvu dekorátora nad
deklaráciu upravovanej funkcie. V tejto chvíli pri zavolaní
write_to_diary()
dostaneme vždy už odokorovaný výstup, teda
reťazec s aktuálnym časom.
Funkcie ako objekty prvej triedy
Zrejme nás prekvapil spôsob, ako s funkciami pracujeme. Naraz ich píšeme bez zátvoriek a odovzdávame ich ako parametre ďalším funkciám! Toto je možné vďaka tomu, že funkcie sú v Pythone takzvané objekty prvej triedy. Znamená to, že s nimi môžeme zaobchádzať rovnako ako s klasickými objektmi. Môžeme ich teda odovzdávať ako parameter iným funkciám a môžeme ich tiež uložiť do premenných. Keď týmto spôsobom pracujeme s funkciou, píšeme jej názov bez zátvoriek – nejde totiž o jej volanie, ale o odovzdávanie funkcie ako takej. Pozrime sa na jednoduchý príklad:
def write_data(): return "Writing data to the database." print(write_data()) print(write_data)
Máme jednoduchú funkciu, ktorá vracia textový reťazec. V prípade, že
zavoláme print(write_data())
, vypíše sa nám do konzoly reťazec
vrátený touto funkciou. Funkciu sme teda zavolali. Keď ale budeme chcieť
vytlačiť funkciu samotnú, teda bez zátvoriek, dostaneme len informáciu o
objekte reprezentujúcom túto funkciu, konkrétne jej pamäťovú adresu:
Konzolová aplikácia
Writing data to the database.
function write_data at 0x000002BA268B04A0
Dekorovanie funkcií s viacerými parametrami
V úvodnom príklade dekorátor pracoval s funkciou, ktorá prijímala iba
jeden parameter, správu k výpisu. Ukážme si, ako vytvoriť
dekorátor, ktorý dokáže pracovať s funkciami, ktoré majú rôzny
počet parametrov V takýchto prípadoch použijeme parametre
*args
a **kwargs
.
Parameter *args
uchováva kolekciu všetkých
pozičných argumentov, zatiaľ čo **kwargs
obsahuje kolekciu všetkých kľúčových argumentov. Pomocou
týchto konštrukcií môžeme k odovzdávaným argumentom pristupovať bez
ohľadu na ich počet či typ. Tento spôsob práce s parametrami zaisťuje
dekorátorom veľkú flexibilitu.
Pozrime sa na príklad funkcie pre výpočet celkovej ceny objednávky. Pomocou dekorátora zaistíme, že k základnej cene bude automaticky pridaná daň:
def add_tax(decorated_function): def add_tax_to_result(*args, **kwargs): price = decorated_function(*args, **kwargs) price_with_tax = price + (args[0] * args[1]) * 0.21 return f"Total order price with tax is: {price_with_tax:.2f} EUR" return add_tax_to_result @add_tax def record_order(price_per_item, quantity, shipping=0, discount=0): return price_per_item * quantity + shipping - discount print(record_order(200, 3, shipping=50, discount=100))
Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.
Vďaka *args
a **kwargs
je náš dekorátor
flexibilný a dokáže dekorovať akúkoľvek funkciu
bez ohľadu na počet a typ jej argumentov. V našom kóde sme
postupovali rovnako ako v predchádzajúcom príklade. Opäť sme v dekorátore
vytvorili vnútornú funkciu add_tax_to_result()
, v ktorej sme k
pôvodnej cene objednávky pridali aj daň. K pozičným argumentom
*args
sme pristúpili cez ich index v hranatých zátvorkách
(args[0]
v našom prípade pre price_per_item
). To
nám umožnilo vziať iba dva parametre (price_per_item
a
quantity
) a z nich vypočítať 21% daň, ktorú sme následne
pripočítali k celkovej cene.
Pre prístup ku kľúčovým argumentom uloženým v **kwargs
by
sme do hranatých zátvoriek uviedli názov kľúča, napríklad
kwargs["discount"]
, ak by sme chceli získať hodnotu parametra
discount
.
Návratovými hodnotami *args
a
**kwargs
sú tuple a slovník. Tieto dátové typy ešte
nepoznáme, zoznámime sa s nimi až v kurze Kolekcie
v Pythone. Obe návratové hodnoty si ale kedykoľvek ľahko prevedieme na
zoznam. Zoznam pozičných argumentov nám vráti volanie
list(args)
a zoznam dvojíc kľúčových argumentov (názov
argumentu, hodnota) volania list(kwargs.items())
.
Viacnásobné dekorovanie
Jednej funkcii môžeme priradiť viac dekorátorov. Hovoríme potom o viacnásobnom dekorovaní. Dekorátory sa aplikujú vo vrstvách, pričom najprv sa aplikuje dekorátor, ktorý je najbližšie k funkcii. Nakoniec sa aplikuje ten, ktorý je v kóde najvyššie. Na zjednodušenie si môžeme predstaviť každý dekorátor ako obal, do ktorého postupne balíme našu funkciu.
Na ukážku pridáme k predchádzajúcemu príkladu ešte náš dekorátor s časovou pečiatkou, ktorý upravíme, aby bol univerzálnejší:
from datetime import datetime def add_tax(decorated_function): def add_tax_to_result(*args, **kwargs): price = decorated_function(*args, **kwargs) price_with_tax = price + (args[0] * args[1]) * 0.21 return f"Total order price with tax is: {price_with_tax:.2f} EUR" return add_tax_to_result def add_timestamp(decorated_function): def modify_message(*args, **kwargs): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") return "[" + timestamp + "] " + str(decorated_function(*args, **kwargs)) return modify_message @add_timestamp @add_tax def record_order(price_per_item, quantity, shipping=0, discount=0): return price_per_item * quantity + shipping - discount print(record_order(200, 3, shipping=50, discount=100))
Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.
V prípade, že zavoláme funkciu record_order()
, Python
najskôr obalí pôvodnú funkciu dekorátorom add_tax()
. Ten
vráti novú funkciu, ktorá po zavolaní vráti reťazec obsahujúci cenu s
pripočítanou daňou. Potom je táto nová funkcia obalená dekorátorom
add_timestamp()
, ktorý upravuje jej správanie tak, že pred
reťazec s cenou a daňou pridá aktuálny čas vo forme časovej pečiatky.
V budúcej lekcii, Dekorátory druhýkrát - Parametrické a triedne dekorátory, budeme v téme dekorátorov pokračovať.