IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

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 funkcie obsah_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ť.


 

Predchádzajúci článok
Riešené úlohy k 13.-14. lekciu Python
Všetky články v sekcii
Objektovo orientované programovanie v Pythone
Preskočiť článok
(neodporúčame)
Dekorátory druhýkrát - Parametrické a triedne dekorátory
Článok pre vás napísal Karel Zaoral
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Karel Zaoral
Aktivity