16. diel - Dekorátory v Pythone
V predchádzajúcom cvičení, Riešené úlohy k 13.-14. lekciu Python, 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 bližšie zoznámime s dekorátormi. Vysvetlíme si, že dekorátor je nástroj, ktorý umožňuje pridať novú funkcionalitu k existujúcim objektom bez modifikácie ich štruktúry. Je to forma metaprogramovania, kde jeden kód dokáže ovplyvňovať iný kód.
Dekorátory v Pythone
Dekorátory v Pythone ponúkajú rad výhod, ktoré zjednodušujú a optimalizujú kód. Umožňujú nám centralizovať opakujúce sa segmenty kódu, miesto ich mnohonásobného písania v rôznych funkciách. Okrem toho vďaka dekorátorom dokážeme pridávať špecifické funkcie, ako je meranie času behu, logovanie alebo overovanie prístupových práv. Navyše vďaka nim dokážeme lepšie oddeliť hlavnú logiku programu od doplnkových funkcií, čo vedie k čistejšej a ľahko čitateľnej štruktúre kódu.
Dekorátory sú náročná téma. Je preto veľmi dôležité starostlivo analyzovať všetky ukážky kódu v lekcii, skúsiť si ich vo vlastnom IDE modifikovať a neprechádzať ďalej v tutoriáli, kým kód skutočne plne nepochopíte.
Základná syntax dekorátorov
Pozrime sa na príklad jednoduchého dekorátora. Ako už názov napovedá, dekorátorom niečo ozdobíme. V našich aplikáciách môžeme dekorovať funkcie a tým ich správanie rozšírime alebo zmeníme. Technicky je dekorátor tiež funkcia. Preto teda pôvodnú funkciu zabalíme do ďalšej funkcie, ktorá správanie tej pôvodnej nejako upraví. To všetko robíme, bez toho aby sme museli zasahovať do kódu pôvodnej funkcie.
V nasledujúcej miniaplikácii často vypisujeme dátum a čas. Namiesto toho, aby sme dávali taký výpis do každej funkcie, vytvoríme dekorátor, ktorý ho do funkcie vloží za nás:
from datetime import datetime
def pridej_casove_razitko(func):
# Dekorátor, ktorý pridáva časové razítko k výstupu funkcie
def zaznamenej_zpravu(zprava):
cas = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return func(f"[{cas}] {zprava}")
return zaznamenej_zpravu
@pridej_casove_razitko
def zapis_do_deniku(zprava):
# Zapisuje správu do denníka s časovým razítkom
print(zprava)
# Použitie dekorovanej funkcie
zapis_do_deniku("Přihlásit se na školení ITnetwork")
Dekorátor pripojí dátum a čas k hodnote parametra, ktorú funkcia
vypisuje. Najprv definujeme funkciu pridej_casove_razitko()
, ktorá
určuje názov dekorátora. Do tejto funkcie pridáme
zaznamenej_zpravu()
, ktorá upraví správanie funkcie pre výpis
správ. Konečne dekorátor pomocou znaku @
k cieľovej funkcii
pripojíme a tým jej správanie zmeníme.
Funkcie ako objekty prvej triedy
Malo by nás zaraziť použitie funkcií func
a
zaznamenej_zpravu
v kóde bez zátvoriek. Ide o kľúčový
koncept v Pythone a týka sa spôsobu, ako Python zaobchádza s
funkciami ako objekty prvej triedy (first-class objects). Keď odkazujeme na
funkciu bez zátvoriek, odkazujeme na funkciu samotnú, nie na výsledok
jej volania. Inými slovami, funkcia je v Pythone objektom ako každý
iný (reťazec, číslo, zoznam a podobne). Preto je možné na ňu odkazovať,
predávať ju ako argument inej funkcii alebo ju vrátiť ako návratovú
hodnotu z inej funkcie. Ukážme si to na jednoduchom príklade:
def zapis_data(): return "Zapisuji data do databáze." def povol_zapis(): return zapis_data vysledek = povol_zapis() print(vysledek) print(vysledek())
Funkcia povol_zapis()
vracia funkciu zapis_data
,
nie jej výsledok. Do premennej vysledek
vkladáme obsah funkcie
povol_zapis()
, čo je referencia na funkciu
zapis_data()
. Ak premennú vypíšeme, dostaneme iba referenciu na
funkciu zapis_data
. Až pridaním zátvoriek voláme kód funkcie
zapis_data()
, ktorý sme obdržali prostredníctvom referencie od
funkcie povol_zapis()
.
Výstup v konzole:
Konzolová aplikácia
function zapis_data at 0x000002BA268B04A0
Zapisuji data do databáze.
Dekorovanie funkcií s viacerými parametrami
Ak chceme dekorovať funkcie, ktoré prijímajú viaceré parametre, musíme
v dekorátore s týmito parametrami správne zaobchádzať. K tomu nám slúžia
*args
a **kwargs
, čo sú nám už známe konvencie
pre zachytenie ľubovoľného počtu pozičných a kľúčových argumentov. V
prvom príklade s časovým razítkom funkcia zaznamenej_zpravu()
očakávala iba jeden parameter, ktorý je rovnaký ako parameter dekorovanej
funkcie zapis_do_deniku()
. Preto tu nebolo nutné použiť
*args
alebo **kwargs
, pretože sme presne vedeli, aký
parameter a koľko parametrov funkcia prijíma.
Predstavme si ale, že očakávame alebo potrebujeme flexibilitu v počte a typoch parametrov funkcie. Toto je vhodné, ak plánujeme dekorátor používať na rôzne funkcie s rôznymi parametrami:
def vypocitej_objem_a_povrch(func):
def vypocitej_objem(*args, **kwargs):
povrch = func(*args, **kwargs)
if len(args) == 3:
a, b, c = args
objem = a * b * c
print(f"Povrch kvádru se stranami a={a}, b={b}, c={c} je: {povrch}")
print(f"Objem kvádru se stranami a={a}, b={b}, c={c} je: {objem}")
return povrch
return vypocitej_objem
@vypocitej_objem_a_povrch
def povrch_kvadru(a, b, c):
return 2 * (a*b + a*c + b*c)
# Použitie dekorovanej funkcie
povrch_kvadru(3, 4, 5)
Vďaka *args
a **kwargs
je náš dekorátor
flexibilný a dokáže dekorovať akúkoľvek funkciu bez ohľadu na to, koľko
a akých argumentov má.
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. Pre
*args
použijeme:
seznam_args = list(args)
Pre **kwargs
získame zoznam dvojíc (názov argumentu, hodnota)
pomocou:
seznam_kwargs = 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 prvý dekorátor, ktorý je aplikovaný, je ten najbližšie funkcii. Posledný dekorátor je ten, ktorý sa nachádza najvyššie.
Pre zjednodušenie si predstavme každý dekorátor ako obal a viacnásobnú dekoráciu potom ako funkciu v niekoľkých obaloch.
Pozrime sa na príklad:
import time
def vypocitej_objem_krychle(func):
def vypocitej_objem_a_obsah(*args, **kwargs):
obsah = func(*args, **kwargs)
objem = obsah * args[0]
print(f"Obsah čtverce se stranou {args[0]} je: {obsah}")
print(f"Objem krychle se stranou {args[0]} je: {objem}")
return obsah
return vypocitej_objem_a_obsah
def validuj_vstup(func):
def over_data(*args, **kwargs):
if any(arg <= 0 for arg in args):
print("Varování: Všechny argumenty musí být kladné a nenulové.")
else:
print("Vstup je korektně zadaný.")
return func(*args, **kwargs)
return over_data
def zmer_cas(func):
def zmer_a_vypis_cas(*args, **kwargs):
zacatek = time.time()
vysledek = func(*args, **kwargs)
konec = time.time()
print(f"Čas běhu funkce obsah_ctverce(): {konec - zacatek:.5f} sekund.")
return vysledek
return zmer_a_vypis_cas
@validuj_vstup
@zmer_cas
@vypocitej_objem_krychle
def obsah_ctverce(a):
time.sleep(1) # na sekundu zdržíme běh programu, jinak je tak rychlý, že bychom dostali čas běhu nulový.
return a**2
obsah_ctverce(3)
V kóde sa krok za krokom stane toto:
- Dekorátor
@validuj_vstup
skontroluje, či je argument kladný a nenulový. - Dekorátor
@zmer_cas
zmeria čas behu funkcieobsah_ctverca()
. - Dekorátor
@vypocitej_objem_krychle
spočíta objem kocky na základe vypočítaného obsahu štvorca.
Akonáhle zavoláme funkciu obsah_ctverca(3)
, v skutočnosti
voláme:
vypocitej_objem_krychle(zmer_cas(validuj_vstup(obsah_ctverca)))(3)
Je teda dôležité si uvedomiť, že dekorátory sa spúšťajú v zostupnom poradí zhora nadol.
Keď odstránime dekorátory nad obsah_ctverca()
a
vložíme vyššie uvedenú konštrukciu na miesto priameho volania
obsah_ctverca(3)
, dostaneme rovnaký výstup, ako pri použití
dekorátorov. Zápis kódu vyššie je veľmi vhodné dôkladne preštudovať -
poskytuje presný obraz toho, ako dekorátory pracujú.
V budúcej lekcii, Dekorátory druhýkrát - Parametrické a triedne dekorátory , budeme v téme dekorátorov pokračovať.