17. diel - Dekorátory druhýkrát - Parametrické a triedne dekorátory
V predchádzajúcej lekcii, Dekorátory v Pythone , sme sa zoznámili s dekorátormi a vysvetlili si princíp ich použitia.
V dnešnom tutoriáli objektovo orientovaného programovania v Pythone budeme pokračovať v práci s dekorátormi. Naučíme sa ich parametrizovať a aplikovať na triedu. Na záver lekcie si tému zhrnieme a ukážeme si celý postup vytvorenia dekorátora v navazujúcich krokoch.
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.
Dekorátory s parametrami
Zatiaľ čo doteraz naše dekorátory prijímali ako argumenty len funkcie, Python vie vytvoriť aj dekorátory, ktoré sami o sebe prijímajú parametre. Vďaka tomu dokážeme jednoducho vytvárať dekorátory, ktoré sa správajú rôzne na základe poskytnutých parametrov.
Využitie dekorátorov s parametrami je veľmi často vidieť napríklad v rôznych webových frameworkoch, kde je možné konfigurovať, ako sa majú funkcie (napr. spracovanie HTTP požiadaviek) správať na základe rôznych argumentov.
Použitím dekorátora s parametrami vytvárame v podstate "továreň na dekorátory":
def zprava_dekorator(zprava):
def pridej_zpravu(func):
def vypis_zpravu(*args, **kwargs):
print(zprava)
return func(*args, **kwargs)
return vypis_zpravu
return pridej_zpravu
zprava = "Volám funkciu pre sčítanie!"
@zprava_dekorator(zprava)
def secti(a, b, c):
print(f"Výsledok výpočtu je: {a + b + c}")
zprava = "Volám funkciu pre násobenie!"
@zprava_dekorator(zprava)
def nasob(a, b, c):
print(f"Výsledok výpočtu je: {a * b * c}")
secti(1, 2, 3)
nasob(10, 20, 30)
Pozrime sa bližšie na kód. Náš "vonkajší" dekorátor
zprava_dekorator()
prijíma argument zprava
a vracia
skutočný dekorátor pridej_zpravu()
. Ten následne obaľuje naše
funkcie secti()
a nasob()
. Vďaka tomu môžeme ľahko
meniť obsah správy pre rôzne funkcie, bez toho aby sme museli meniť samotný
dekorátor. Vnútorná funkcia vypis_zpravu()
pozná hodnotu
premennej zprava
iba z takzvaného vonkajšieho
kontextu, čo je ukážkou mechanizmu zvaného
closure.
Keď teda chceme vytvoriť dekorátor s parametrami, potrebujeme tri úrovne funkcií:
- vonkajšiu funkciu, ktorá prijíma parametre dekorátora,
- vnútornú funkciu (dekorátor), ktorá prijíma funkciu, ktorú chceme dekorovať,
- obalenú funkciu - to je tá skutočná funkcia, ktorá rozširuje správanie pôvodnej dekorovanej funkcie a je zavolaná namiesto nej.
Toto je práve tá "továreň na dekorátory" - možnosť vytvoriť dekorátor na mieru podľa našich potrieb.
Uzáver (closure)
Uzáver (closure) je funkcia, ktorá si "pamätá" svoje voľné premenné z okolitých kontextov, v ktorých bola definovaná, a dokáže k nim pristupovať aj po skončení tohto kontextu. Jednoducho povedané, closure je funkcia spolu s nejakým zachyteným kontextom. V kóde je tento "kontext" tvorený premennými, ktoré sú dostupné v okamihu vytvorenia closure:
def vydel_cislo(delenec):
def deleni(delitel):
return delenec / delitel
return deleni
delici_funkce = vydel_cislo(10)
print(delici_funkce(5)) # Výstup: 2
V tomto príklade je funkcia deleni()
uzáverom, ktorý má
prístup k premennej delenec
aj po tom, čo funkcia
vydel_cislo()
skončila.
Vysvetlenie, prečo si funkcia "pamätá" svoj kontext, je spojené s tým,
ako Python funguje "pod kapotou". Uzávery v Pythone sú realizované
prostredníctvom objektu, ktorý reprezentuje funkciu. Tento objekt obsahuje
niekoľko atribútov, ktoré uchovávajú informácie o funkcii a jej kontexte.
Jedným z týchto atribútov je __closure__
, ktorý obsahuje
referencie na voľné premenné z kontextu, kde bola funkcia
vytvorená. Keď definujeme vnorenú funkciu vnútri inej funkcie a táto
vnorená funkcia odkazuje na premenné z vonkajšej funkcie, Python closure
vytvorí automaticky.
Triedne dekorátory
Rovnako ako sme vytvárali dekorátory pre funkcie, budeme tiež tvoriť dekorátory pre triedy. Triedne dekorátory obvykle pridávajú, upravujú alebo rozširujú funkcionalitu triedy.
Rovnako ako pri funkčných dekorátoroch, tak aj triedny dekorátor je funkciou, ktorá prijíma triedu ako argument a vracia upravenú alebo novú triedu. Pozrime sa na príklad:
def bezpecnostni_overeni(trida):
class UrovenOpravneni(trida):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Predpokladáme, že každý zamestnanec má atribút 'uroven_opravneni'
self.uroven_opravneni = kwargs.get('uroven_opravneni', 0)
def zobraz_citlive_informace(self):
if self.uroven_opravneni >= 5: # Predpokladajme, že oprávnenie 5 je potrebné pre prístup
return super().zobraz_citlive_informace()
else:
return " nemá oprávnenie pre zobrazenie citlivých informácií."
return UrovenOpravneni
@bezpecnostni_overeni
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"Zamestnanec: {self.jmeno}, Pozícia: {self.pozice}"
def zobraz_citlive_informace(self):
return ": citlivé informácie o zamestnancovi a firme..."
# Zamestnanec s nízkou úrovňou oprávnenia
zamestnanec_jan = Zamestnanec("Jan Novák", "Vývojár", uroven_opravneni=1)
print(zamestnanec_jan.jmeno + zamestnanec_jan.zobraz_citlive_informace())
# Zamestnanec s vysokou úrovňou oprávnenia
zamestnanec_petr = Zamestnanec("Petr Sýkora", "Manažér", uroven_opravneni=5)
print(zamestnanec_petr.jmeno + zamestnanec_petr.zobraz_citlive_informace())
Výhodami triednych dekorátorov sú:
- modularita - oddelíme rôzne funkcionality do rôznych dekorátorov a aplikujeme ich podľa potreby,
- opakovaná použiteľnosť - raz vytvorený dekorátor je možné použiť na viac triedach,
- rozšíriteľnosť - ľahko rozšírime funkcie existujúcich tried bez úpravy pôvodného kódu.
Pozor si musíme dať na:
- komplexitu - rovnako ako pri funkčných dekorátoroch je dôležité nepreháňať to s príliš mnohými funkciami v jednom dekorátore. Výsledkom budú zmätky a komplikácie pri čítaní kódu,
- dedičnosť - dekorátor samozrejme interaguje s dedičnosťou. Ak trieda dedí z inej triedy, dekorátor môže výrazne ovplyvniť správanie potomka.
Kód obsahuje zaujímavú časť, ktorú budeme preberať až v kurze Kolekcie. jedná sa o riadok:
self.uroven_opravneni = kwargs.get('uroven_opravneni', 0)
Tento kód využíva takzvaný slovník a jeho metódu get()
pre
získanie hodnoty kľúča uroven_opravneni
z kwargs
.
Metóda súčasne zabezpečí, že ak v kwargs
kľúč
uroven_opravneni
nebude špecifikovaný, nastaví sa jeho hodnota
na 0
.
Vytváranie triednych dekorátorov je už naozaj veľmi pokročilá technika (aj samotné funkčné dekorátory nie sú úplne triviálne), ale je neoceniteľná v určitých situáciách, keď potrebujeme meniť správanie tried dynamicky a modulárne.
Vstavané dekorátory
Python ponúka niekoľko vstavaných dekorátorov, ktoré umožňujú rýchlo
a efektívne rozšíriť funkcionalitu vašich tried a funkcií. V kurze už sme
sa zoznámili s dekorátormi @staticmethod
a
@classmethod
. S dekorátorom @property
sa zoznámime v
lekcii Vlastnosti v
Pythone. Vstavané dekorátory v Pythone uľahčujú radu bežných
programátorských úloh a umožňujú efektívnu a elegantnú implementáciu
funkcionalít. Je naozaj dôležité sa s nimi dobre zoznámiť, pretože ich
budeme často stretávať v praxi.
Vytváranie vlastných dekorátorov
Už sme si ukázali, ako dekorátory fungujú, a videli sme, ako dokážu meniť správanie funkcií, bez toho aby sme ich (tie funkcie) museli priamo upravovať. Pretože ide o pomerne náročnú tému, celú lekciu si teraz zhrnieme a pozrieme sa, ako vytvoriť vlastný dekorátor od základu.
Návrh dekorátora
Základným krokom pri vytváraní dekorátora je napísať funkciu (t. j. dekorátor), ktorý prijíma funkciu ako argument a vracia inú funkciu:
def doba_behu_funkce(func): def zmer_cas(): # kód začne merať čas pred volaním pôvodnej funkcie func() # funkcia, ktorú budeme chcieť dekorovať # kód ukončí meranie času po volaní pôvodnej funkcie return zmer_cas
Parametrizácia dekorátora
Ako sme si ukázali, dekorátor vie prijímať parametre. K tomu potrebujeme ďalšiu vonkajšiu funkciu, ktorá obklopí náš dekorátor:
def mereni_behu_funkce(povoleno=true): def doba_behu_funkce(func): def zmer_cas(): if povoleno: # kód pred funkciou func() # funkcia, ktorú budeme chcieť dekorovať # kód po funkcii return zmer_cas return doba_behu_funkce
Použitie dekorátora
K aplikácii dekorátora na funkciu použijeme @
syntax:
import time
def mereni_behu_funkce(povoleno):
def doba_behu_funkce(func):
def zmer_cas(*args, **kwargs):
if povoleno:
zacatek = time.time()
print("Začínam merať dĺžku behu funkcie scitej().")
vysledek = func(*args, **kwargs) # Volá pôvodnú funkciu s predanými argumentmi.
konec = time.time()
print("Dokončil som meranie.")
print(f"Čas behu funkcie {func.__name__}: {konec - zacatek:.5f} sekúnd.")
return vysledek
else:
print("Meranie neprebehlo.")
return func(*args, **kwargs) # Ak nie je povolené meranie, volá pôvodnú funkciu.
return zmer_cas
return doba_behu_funkce
@mereni_behu_funkce(povoleno=True)
def scitej(a=10, b=20):
print("10 + 20 je", a + b)
time.sleep(1) # Simulácia dlhšieho behu funkcie.
scitej()
Volanie scitej()
je možné bez použitia
@mereni_behu_funkce(povoleno)
nahradiť zápisom
mereni_behu_funkce(povoleno=True)(scitej)()
. Keď každý
náš vytvorený dekorátor dokážeme zapísať aj týmto spôsobom,
je to dobrá známka toho, že problematike dobre rozumieme.
Dekorátory sú silným nástrojom, ak sú používané správne. Umožňujú nám dodať dodatočné správanie funkciám alebo triedam v modulárnej a čitateľnej forme. Je ale veľmi dôležité dbať na to, aby kód zostal čitateľný a nesnažiť sa napchať za každú cenu príliš veľa funkcionality do jedného dekorátora.
V budúcej lekcii, Vlastnosti v Pythone, sa budeme zaoberať vlastnosťami čiže gettery a settery, ktoré umožnia jednoduchšie nastavovanie a validáciu hodnôt atribútov.