Python - Dekoratori (Decorators)


Dekoratori u Pythonu

Python ima zanimljivu karakteristiku koja se naziva dekoratori za dodavanje funkcionalnosti postojećem kodu. To se takođe naziva metaprogramiranjem, jer dio programa pokušava modifikovati drugi dio programa u vrijeme kompajliranja.



Preduslovi za učenje dekoratera

Da bismo razumjeli dekoratere, prvo moramo znati nekoliko osnovnih stvari u Pythonu. Moramo biti zadovoljni činjenicom da je sve u Pythonu (da! Čak i klase) objekti. Imena koja definišemo su jednostavno identifikatori vezani za ove objekte. Funkcije nisu izuzetci, one su i objekti (s atributima). Na isti funkcijski objekt mogu se vezati različita imena. Pogledajmo primjer:

def first(msg):
    print(msg)


first("Hello")

second = first
second("Hello")

Kada pokrenete kod, obe funkcije prva i druga daju isti izlaz. Ovdje se imena prva i druga odnose na isti objekt funkcije. Sad stvari postaju čudnije. Funkcije se mogu prosljediti kao argumenti drugoj funkciji. Ako ste koristili funkcije kao što su: map, filter i reduce u Pythonu, tada već znate za ovo. To su funkcije koje uzimaju druge funkcije kao argumente, nazivaju se i funkcijama višeg reda. Evo primjera takve funkcije.

def inc(x):
    return x + 1

def dec(x):
    return x - 1

def operate(func, x):
    result = func(x)
    return result

Funkciju pozivamo na sljedeći način.

>>> operate(inc,3)
4
>>> operate(dec,3)
2

Nadalje, funkcija može vratiti drugu funkciju.

def is_called():
    def is_returned():
        print("Hello")
    return is_returned

new = is_called()

# Ispisuje "Hello"
new()

Ovdje je is_returned() ugniježdena funkcija koja se definiše i vraća svaki put kada pozovemo is_called(). Konačno, moramo znati o zatvaračima u Pythonu.



Povratak na Dekoratore

Funkcije i metode nazivaju se callable, kao što se i mogu nazvati. U stvari, bilo koji objekt koji implementuje posebnu __call__() metodu naziva se callable. Dakle, u najosnovnijem smislu, dekorator je callable koji vraća callable. U osnovi, dekorator uzima funkciju, dodaje neku funkcionalnost i vraća je.

def make_pretty(func):
    def inner():
        print("Odlikovao sam se")
        func()
    return inner

def ordinary():
    print("Ja sam običan")

Kada pokrenete sljedeće kodove u Python shell,

>>> ordinary()
I am ordinary

>>> # ukrasimo ovu uobičajenu funkciju
>>> pretty = make_pretty(ordinary)
>>> pretty()
Odlikovao sam se
Ja sam običan

U gore prikazanom primjeru make_pretty() je dekorater. U koraku dodjele:

pretty = make_pretty(ordinary)

Funkcija regular() je dekorisana, a vraćena funkcija je dobila ime pretty. Vidimo da je funkcija dekoratora dodala novu funkciju originalnoj funkciji. Ovo je slično pakovanju poklona. Dekorator djeluje kao omot. Priroda predmeta koji je ukrašen (stvarni poklon unutra) se ne mijenja. Ali sada izgleda prilično (otkad je ukrašen). Uopšteno, dekorištemo funkciju i dodjeljujemo je kao,

ordinary = make_pretty(ordinary).

Ovo je uobičajena konstrukcija i iz tog razloga Python ima sintaksu da to pojednostavi. Možemo upotrijebiti simbol @ zajedno s nazivom funkcije dekoratera i postaviti ga iznad definicije funkcije koja se dekoriše. Na primjer:

@make_pretty
def ordinary():
    print("Ja sam običan")

je ekvivalentno sa:

def ordinary():
    print("Ja sam običan")
ordinary = make_pretty(ordinary)

Ovo je samo sintaksički ukras za primjenu dekoratora.



Dekoracija funkcija parametrima

Gornji dekorator bio je jednostavan i radio je samo s funkcijama koje nisu imale nikakve parametre. Što bi bilo da imamo funkcije koje uzimaju parametre poput:

def divide(a, b):
    return a/b

Ova funkcija ima dva parametra, a i b. Znamo da će dati grešku ako u b dodamo 0.

>>> divide(2,5)
0.4
>>> divide(2,0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero

Sada napravimo dekoraciju koja će provjeriti postoji li slučaj koji će uzrokovati grešku.

def smart_divide(func):
    def inner(a, b):
        print("Ja ću podijeliti", a, "i", b)
        if b == 0:
            print("Whoops! ne mogu podijeliti")
            return

        return func(a, b)
    return inner

@smart_divide
def divide(a, b):
    print(a/b)

Ova nova implementacija vratiće None ako se pojavi uslov greške.

>>> divide(2,5)
Ja ću podijeliti 2 i 5
0.4

>>> divide(2,0)
Ja ću podijeliti 2 i 0
Whoops! ne mogu podijeliti

Na taj način možemo dekorisati funkcije koje uzimaju parametre. Oštri promatrač primijetit će da su parametri ugniježdene unutar inner() funkcije dekoratera jednakim parametrima funkcija koju dekoriše. Uzimajući to u obzir, sada možemo napraviti opšte dekoratere koji rade s bilo kojim brojem parametara. U Pythonu se ova magija vrši kao funkcija (*args, **kwargs). Na taj način, args će biti skup pozicijskih argumenata, a kwargs će biti rječnik argumenata ključnih riječi. Primjer takvog dekoratera bi će:

def works_for_all(func):
    def inner(*args, **kwargs):
        print("Mogu dekorisati bilo koju funkciju")
        return func(*args, **kwargs)
    return inner


Lanci za ukrašavanje u Pythonu (Chaining Decorators)

U Python se može povezati više dekoratera. Funkcija se može više puta dekorisati različitim (ili istim) dekoraterima. Jednostavno postavimo dekoratere iznad željene funkcije.

def star(func):
    def inner(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)
    return inner

def percent(func):
    def inner(*args, **kwargs):
        print("%" * 30)
        func(*args, **kwargs)
        print("%" * 30)
    return inner

@star
@percent
def printer(msg):
    print(msg)

printer("Hello")