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ľ naše dekorátory pracovali iba s dekorovanými funkciami. V Pythone je však možné vytvoriť dekorátory, ktoré prijímajú aj vlastné parametre. To nám umožňuje vytvárať flexibilnejšie dekorátory, ktorých správanie je možné prispôsobiť na základe zadaných parametrov. Tie by sme odovzdali pri aplikácii dekorátora podobne ako pri volaní bežnej funkcie:
@measure_performance(unit='ms') def save_data_to_database(data): # ...
Využitie dekorátorov s parametrami je veľmi často vidieť napríklad v rôznych webových frameworkoch, ako je Django, kde je možné konfigurovať správanie funkcií na základe rôznych argumentov, napríklad zabezpečenie prístupu na základe oprávnení:
@permission_required('auth.change_user', raise_exception=True) def edit_user(request): # ...
Použitím dekorátora s parametrami vytvárame v podstate „továreň na dekorátory“.
Teraz si teda vytvoríme dekorátor s parametrom, ktorý umožní odovzdať správu pred volaním ľubovoľnej funkcie:
def add_message(message): def add_message_decorator(decorated_function): def print_message(*args, **kwargs): print(message) return decorated_function(*args, **kwargs) return print_message return add_message_decorator message = "Calling the addition function!" @add_message(message) def add(a, b, c): print(f"The result of the calculation is: {a + b + c}") message = "Calling the multiplication function!" @add_message(message) def multiply(a, b, c): print(f"The result of the calculation is: {a * b * c}") add(1, 2, 3) multiply(10, 20, 30)
Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.
Pozrime sa bližšie na kód. Náš "vonkajší" dekorátor
add_message()
prijíma argument message
a vracia
skutočný dekorátor add_message_decorator()
. Ten následne
obaľuje naše funkcie add()
a multiply()
. 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 print_message()
pozná hodnotu premennej message
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 je špeciálny typ funkcie, ktorá si "pamätá" premenné z okolia, kde bola vytvorená, a dokáže ich používať, aj keď toto okolie (čiže kontext) už neexistuje. Inými slovami, uzáver je funkcia, ktorá si "nesie so sebou" dáta , ktorá bola dostupná vo chvíli, keď vznikla:
def divide_number(dividend): def divide_by(divisor): return dividend / divisor return divide_by division_function = divide_number(10) print(division_function(5))
Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.
V našom príklade sme vytvorili uzáver uložením funkcie s parametrom
10
do premennej division_function
. Táto funkcia si od
tejto chvíle pamätá onú hodnotu, ktorá bola nastavená pri vytvorení
uzáveru, a dokáže ju použiť kedykoľvek neskôr. Pri následnom zavolaní
funkcie division_function(5)
sa použije uložená hodnota
10
ako delenec, ktorý sa vydelí hodnotou v parametri
(5
), čo vráti výsledok 2.0
.
Aj keď funkcia divide_number()
(kde bol uzáver vytvorený) už
skončila, hodnota premennej dividend
je stále dostupná vďaka
tomu, že ju Python automaticky uložil spolu s funkciou
divide_by()
. To je presne princíp uzáveru – funkcia si pamätá
a uchováva svoj pôvodný kontext, a preto s ním môže ďalej pracovať.
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 by bola funkcia vytvorená.
Tento mechanizmus je užitočný, pretože umožňuje vytvárať prispôsobené funkcie, ktoré si uchovávajú svoje vlastné hodnoty bez nutnosti používať globálne premenné alebo zložité štruktúry. Python tento proces automatizuje, takže uzávery fungujú jednoducho a bez zložitého nastavovania. Funkcia jednoducho získa prístup k premenným z kontextu, v ktorom bola vytvorená.
Triedne dekorátory
Rovnako ako sme vytvárali dekorátory pre funkcie, môžeme aj vytvoriť dekorátory pre triedy. Triedne dekorátory obvykle pridávajú, upravujú alebo rozširujú funkcionalitu triedy.
Rovnako ako u predchádzajúcich dekorátorov, 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 check_permission_level(cls): class PermissionLevel(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.permission_level = kwargs.get('permission_level', 0) def display_sensitive_information(self): if self.permission_level >= 5: return f"[Access granted]: {super().display_sensitive_information()}" else: return f"[Access denied]: Insufficient permission level (level {self.permission_level}/5)." return PermissionLevel @check_permission_level class Employee: def __init__(self, name, position, permission_level=0): self.name = name self.position = position self.permission_level = permission_level def display_information(self): return f"Employee: {self.name}, Position: {self.position}" def display_sensitive_information(self): return "Sensitive informations about the employee and the company..." employee_marc = Employee("Marc Graham", "Developer", permission_level=1) print(f"{employee_marc.name}: {employee_marc.display_sensitive_information()}") employee_peter = Employee("Peter Nightingale", "Manager", permission_level=5) print(f"{employee_peter.name}: {employee_peter.display_sensitive_information()}")
Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.
V príklade vytvárame dekorátor check_permission_level()
,
ktorý pracuje s triedou namiesto funkcie. Dekorátor prepisuje konštruktor
triedy a pridáva logiku na kontrolu oprávnenia. Konkrétne upravuje metódu
display_sensitive_information()
, aby pri nedostatočnej úrovni
oprávnenia zobrazila chybovú hlášku namiesto pôvodného výstupu.
Dekorátor používame nad deklaráciou triedy pomocou zápisu
@check_permission_level
. V prípade, že máme zamestnancov s
úrovňou oprávnenia menšou ako 5
, pri zavolaní jeho metódy
display_sensitive_information()
teraz dostávame správu o
nedostatočnej miere oprávnenia.
Metóda get()
kolekcie
slovník
Kód obsahuje doposiaľ neprebranú látku, s ktorou sa stretneme až v kurze Kolekcie v Pythone. Ide o riadok:
self.permission_level = kwargs.get('permission_level', 0)
Tento kód využíva takzvaný slovník a jeho metódu get()
na
získanie hodnoty kľúča permission_level
z kwargs
.
Metóda súčasne zaisťuje, že pokiaľ v kwargs
kľúč
permission_level
nebude špecifikovaný, nastaví sa jeho hodnota
na 0
.
Použitie triednych dekorátorov
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.
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 (dekorátor), ktorý prijíma funkciu ako argument a vracia inú funkciu:
def measure_function_runtime(decorated_function): def measure_time(): # some code before calling the original function decorated_function() # some code after calling the original function return measure_time
Tento dekorátor measure_function_runtime()
definuje vnorenú
funkciu measure_time()
, ktorá zavolá odovzdanú funkciu
decorated_function()
. Dekorátor vracia funkciu
measure_time()
, ktorá by mohla byť použitá na sledovanie alebo
meranie behu funkcie, ak by bola doplnená o časové meranie (napríklad
pomocou modulu time
).
Parametrizácia dekorátora
Ako sme si ukázali, dekorátor vie prijímať parametre, kód upravíme:
def set_number_of_measurements(number_of_measurements=1): def measure_function_runtime(decorated_function): def measure_time(): # ... decorated_function() # ... return measure_time return measure_function_runtime
Tento kód obaľuje predchádzajúcu ukážku ďalšou vrstvou - funkciou
set_number_of_measurements()
. Tá prijíma v parametri počet
meraní a vracia dekorátor measure_function_runtime()
. Ten
obaľuje odovzdanú funkciu decorated_function()
do vnorenej
funkcie measure_time()
.
Použitie dekorátora
Do tela dekorátora doplníme implementáciu merania behu funkcie a
dekorátor aplikujeme na funkciu add()
pomocou @
syntaxe:
import time def set_number_of_measurements(number_of_measurements=1): def measure_function_runtime(decorated_function): def measure_time(*args, **kwargs): runtimes = [] print(f"Starting to measure the runtime of the function {decorated_function.__name__}.") for i in range(number_of_measurements): start = time.time() decorated_function(*args, **kwargs) end = time.time() runtime = end - start runtimes.append(runtime) print(f"Measurement {i + 1}/{number_of_measurements}: Runtime = {runtime:.6f} s.") average_runtime = sum(runtimes) / number_of_measurements print(f"Average runtime of the function {decorated_function.__name__} after {number_of_measurements} measurements: {average_runtime:.6f} s.") return measure_time return measure_function_runtime @set_number_of_measurements(2) def add(a=10, b=20): print(f"{a} + {b} is {a + b}") time.sleep(0.225) # Simulating a longer function runtime. add()
Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.
Volanie dekorovanej funkcie add()
je možné
nahradiť zápisom set_number_of_measurements(2)(add)()
. Keď
každý náš vytvorený dekorátor dokážeme zapísať aj týmto
spôsobom, je to dobrá známka toho, že problematike 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.