Објашњено (са примерима и случајевима коришћења)

Razumevanje Python Dekoratera

Python dekorateri predstavljaju izuzetno koristan alat u programskom jeziku Python. Oni omogućavaju izmenu ponašanja funkcija tako što ih obavijaju u druge funkcije. Dekorateri nam omogućavaju da pišemo čistiji kod i delimo funkcionalnosti na efikasan način. Ovaj članak je detaljan vodič ne samo o upotrebi, već i o kreiranju sopstvenih dekoratera.

Neophodno predznanje

Tema dekoratera u Python-u zahteva određeno osnovno znanje. U nastavku su navedeni koncepti sa kojima bi trebalo da budete upoznati kako biste lakše razumeli ovaj vodič. Takođe su priloženi linkovi ka resursima gde možete obnoviti te koncepte ukoliko je potrebno.

Osnove Python-a

Ova tema spada u srednje naprednu kategoriju. Zato, pre nego što počnete sa učenjem, trebalo bi da budete upoznati sa osnovama Python-a, kao što su tipovi podataka, funkcije, objekti i klase. Takođe je poželjno razumevanje koncepta objektno orijentisanog programiranja, uključujući getere, setere i konstruktore. Ako niste upoznati sa Python-om, evo nekoliko resursa za početak.

Funkcije kao objekti prvog reda

Pored osnovnog Python-a, važno je da razumete i napredniji koncept. U Python-u su funkcije objekti, baš kao i int ili string. S obzirom na to da su objekti, možete ih koristiti na različite načine:

  • Možete proslediti funkciju kao argument drugoj funkciji, baš kao što biste prosledili string ili int.
  • Funkcije mogu vraćati druge funkcije, slično vraćanju string ili int vrednosti.
  • Funkcije se mogu čuvati u promenljivim.

Zapravo, jedina razlika između funkcionalnih objekata i ostalih objekata je u tome što funkcionalni objekti sadrže magičnu metodu __call__().

Nadam se da ste u ovom trenutku zadovoljni sa potrebnim predznanjem. Sada možemo da pređemo na glavnu temu.

Šta je Python dekorater?

Python dekorater je funkcija koja prima drugu funkciju kao argument i vraća izmenjenu verziju te funkcije. Drugim rečima, funkcija foo je dekorater ako kao argument uzima funkciju bar i vraća drugu funkciju baz.

Funkcija baz je modifikacija funkcije bar, gde unutar tela baz postoji poziv funkcije bar. Međutim, pre i posle poziva bar, funkcija baz može izvršiti bilo koju operaciju. Da bi ovo bilo jasnije, sledi primer koda:

# Foo je dekorater, prima drugu funkciju, bar, kao argument
def foo(bar):

    # Kreiramo baz, modifikovanu verziju bar
    # baz će pozvati bar, ali može izvršiti bilo šta pre i posle poziva funkcije
    def baz():

        # Pre poziva bar, štampamo nešto
        print("Nešto")

        # Zatim pokrećemo bar pozivom funkcije
        bar()

        # Zatim štampamo nešto drugo nakon pokretanja bar
        print("Nešto drugo")

    # Na kraju, foo vraća baz, modifikovanu verziju bar
    return baz

Kako kreirati dekorater u Python-u?

Kako bih ilustrovao kreiranje i upotrebu dekoratera u Python-u, koristiću jednostavan primer. Kreiraćemo funkciju dekoratera logger, koja će beležiti ime funkcije koju ukrašava svaki put kada se ta funkcija izvrši.

Za početak, kreiramo funkciju dekoratera. Dekorater prima funkciju kao argument. func je funkcija koju ukrašavamo.

def create_logger(func):
    # Telo funkcije ide ovde

Unutar funkcije dekoratera, kreiraćemo našu modifikovanu funkciju koja će zabeležiti ime func pre pokretanja same func.

# Unutar create_logger
def modified_func():
    print("Poziva se: ", func.__name__)
    func()

Zatim, funkcija create_logger će vratiti izmenjenu funkciju. Kao rezultat, cela naša funkcija create_logger će izgledati ovako:

def create_logger(func):
    def modified_func():
        print("Poziva se: ", func.__name__)
        func()

    return modified_func

Završili smo sa kreiranjem dekoratera. Funkcija create_logger je jednostavan primer funkcije dekoratera. Ona prima func, funkciju koju ukrašavamo, i vraća drugu funkciju, modified_func. modified_func prvo beleži ime func, a zatim pokreće samu func.

Kako koristiti dekoratere u Python-u

Da bismo koristili naš dekorater, koristimo @ sintaksu na sledeći način:

@create_logger
def say_hello():
    print("Zdravo, Svete!")

Sada možemo pozvati say_hello() u našem skriptu, i izlaz bi trebao biti sledeći:

Poziva se:  say_hello
"Zdravo, Svete!"

Šta zapravo radi @create_logger? To je primena dekoratera na našu say_hello funkciju. Da bismo bolje razumeli šta se dešava, kod neposredno ispod ovog pasusa bi postigao isti rezultat kao i stavljanje @create_logger ispred say_hello.

def say_hello():
    print("Zdravo, Svete!")

say_hello = create_logger(say_hello)

Drugim rečima, jedan od načina korišćenja dekoratera u Python-u je da eksplicitno pozovete dekorater prosleđujući funkciju, kao što smo uradili u gornjem kodu. Drugi, sažetiji način, je upotreba @ sintakse.

U ovom odeljku smo opisali kako se kreiraju Python dekorateri.

Malo komplikovaniji primeri

Gornji primer je bio jednostavan. Postoje malo složeniji slučajevi, kao što je kada funkcija koju ukrašavamo prima argumente. Druga složenija situacija je kada želite da ukrasite celu klasu. Ovde ću objasniti obe situacije.

Kada funkcija prima argumente

Kada funkcija koju ukrašavate prima argumente, modifikovana funkcija mora da primi argumente i prosledi ih kada na kraju pozove neizmenjenu funkciju. Ako ovo zvuči zbunjujuće, dozvolite da objasnim koristeći foo-bar terminologiju.

Podsetimo se da je foo dekoraterska funkcija, bar je funkcija koju ukrašavamo, a baz je ukrašena verzija bar. U tom slučaju, bar će primati argumente i prosleđivati ih baz prilikom poziva. Primer koda koji učvršćuje koncept:

def foo(bar):
    def baz(*args, **kwargs):
        # Ovde možete nešto uraditi
        ___
        # Zatim pozivamo bar, prosleđujući args i kwargs
        bar(*args, **kwargs)
        # Ovde takođe možete nešto uraditi
        ___

    return baz

Ako *args i **kwargs izgledaju nepoznato, oni su pokazivači na pozicione argumente i ključne reči, respektivno.

Važno je napomenuti da baz ima pristup argumentima i stoga može izvršiti proveru argumenata pre nego što pozove bar.

Primer bi bio funkcija dekoratera, ensure_string, koja osigurava da je argument prosleđen funkciji koju ukrašava string. Implementirali bismo je ovako:

def ensure_string(func):
    def decorated_func(text):
        if type(text) is not str:
             raise TypeError('Argument funkcije ' + func.__name__ + ' mora biti string.')
        else:
             func(text)

    return decorated_func

Mogli bismo ukrasiti funkciju say_hello ovako:

@ensure_string
def say_hello(name):
    print('Zdravo', name)

Zatim bismo mogli testirati kod koristeći ovo:

say_hello('John') # Trebalo bi da radi bez problema
say_hello(3) # Trebalo bi da baci izuzetak

I trebalo bi da dobijemo sledeći izlaz:

Zdravo John
Traceback (most recent call last):
   File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in  say_hello(3) # trebalo bi da baci izuzetak
   File "/home/anesu/Documents/python-tutorial/./decorators.py", line 7, in decorated_func raise TypeError('Argument funkcije + func.__name__ + mora biti string.')
TypeError: Argument funkcije say_hello mora biti string. $0

Kao što se i očekivalo, program je uspeo da odštampa ‘Zdravo John’ jer je ‘John’ string. Izbacio je izuzetak kada je pokušao da odštampa ‘Zdravo 3’ jer ‘3’ nije bio string. Dekorater ensure_string se može koristiti za validaciju argumenata bilo koje funkcije koja zahteva string.

Dekorisanje klase

Osim što ukrašavamo funkcije, možemo ukrasiti i klase. Kada dodate dekorater klasi, dekorišani metod zamenjuje metod konstruktora/inicijatora klase (__init__).

Vraćajući se na foo-bar primer, pretpostavimo da je foo naš dekorater, a Bar klasa koju ukrašavamo. Tada će foo ukrasiti Bar.__init__. Ovo će biti korisno ako želimo da uradimo nešto pre nego što se kreiraju objekti tipa Bar.

To znači da sledeći kod:

def foo(func):
    def new_func(*args, **kwargs):
        print('Izvršava se nešto pre instanciranja')
        func(*args, **kwargs)

    return new_func

@foo
class Bar:
    def __init__(self):
        print("U inicijatoru")

Je ekvivalentan sledećem:

def foo(func):
    def new_func(*args, **kwargs):
        print('Izvršava se nešto pre instanciranja')
        func(*args, **kwargs)

    return new_func

class Bar:
    def __init__(self):
        print("U inicijatoru")


Bar.__init__ = foo(Bar.__init__)

U stvari, instanciranje objekta klase Bar, definisanog korišćenjem bilo koje od dve metode, trebalo bi da pruži isti izlaz:

Izvršava se nešto pre instanciranja
U inicijatoru

Primeri dekoratera u Python-u

Iako možete definisati svoje dekoratere, postoje neki koji su već ugrađeni u Python. Evo nekih od uobičajenih dekoratera na koje možete naići:

@staticmethod

@staticmethod se koristi za klasu da naznači da je metod koji ukrašava statički metod. Statički metodi su metodi koji se mogu pokrenuti bez potrebe za instanciranjem klase. U sledećem primeru koda kreiramo klasu Dog sa statičkim metodom bark.

class Dog:
    @staticmethod
    def bark():
        print('Vau, vau!')

Sada se metodu bark može pristupiti ovako:

Dog.bark()

A pokretanje koda bi proizvelo sledeći izlaz:

Vau, vau!

Kao što sam pomenuo u odeljku „Kako se koriste dekorateri“, dekorateri se mogu koristiti na dva načina. Sintaksa @ je sažetija, dok je drugi metod da pozovete funkciju dekoratera, prosleđujući funkciju koju želimo da ukrasimo kao argument. To znači da gornji kod postiže istu stvar kao kod ispod:

class Dog:
    def bark():
        print('Vau, vau!')

Dog.bark = staticmethod(Dog.bark)

I dalje možemo koristiti metod bark na isti način:

Dog.bark()

I to bi proizvelo isti izlaz:

Vau, vau!

Kao što vidite, prvi metod je čistiji i očiglednije je da je funkcija statička funkcija pre nego što ste uopšte počeli da čitate kod. Zato ću za preostale primere koristiti prvi metod. Ali zapamtite da je drugi metod alternativa.

@classmethod

Ovaj dekorater se koristi da označi da je metod koji ukrašava metod klase. Metodi klase su slični statičkim metodima po tome što oba ne zahtevaju da se klasa instancira pre nego što se mogu pozvati.

Međutim, glavna razlika je u tome što metodi klase imaju pristup atributima klase, dok statički metodi nemaju. To je zato što Python automatski prosleđuje klasu kao prvi argument metodu klase kad god se ona pozove. Da bismo kreirali metod klase u Python-u, možemo koristiti dekorater classmethod.

class Dog:
    @classmethod
    def what_are_you(cls):
        print("Ja sam " + cls.__name__ + "!")

Da bismo pokrenuli kod, jednostavno pozivamo metod bez instanciranja klase:

Dog.what_are_you()

A izlaz je:

Ja sam Dog!

@property

Dekorater property se koristi za označavanje metoda kao parametra svojstva. Vraćajući se na naš primer psa, kreiraćemo metod koji uzima ime psa.

class Dog:
    # Kreiramo konstruktor koji prima ime psa
    def __init__(self, name):

         # Kreiramo privatno svojstvo ime
         # Dupla podvlaka čini atribut privatnim
         self.__name = name

    
    @property
    def name(self):
        return self.__name

Sada možemo pristupiti imenu psa kao normalnom svojstvu:

# Kreiranje instance klase
foo = Dog('foo')

# Pristupanje svojstvu name
print("Ime psa je:", foo.name)

A rezultat pokretanja koda bi bio:

Ime psa je: foo

@property.setter

Dekorater property.setter se koristi za kreiranje metoda setera za naša svojstva. Da biste koristili dekorater @property.setter, zamenite svojstvo imenom svojstva za koje kreirate seter. Na primer, ako kreirate seter za metod za svojstvo foo, vaš dekorater će biti @foo.setter. Evo primera psa za ilustraciju:

class Dog:
    # Kreiranje konstruktora koji prima ime psa
    def __init__(self, name):

         # Kreiramo privatno svojstvo ime
         # Dupla podvlaka čini atribut privatnim
         self.__name = name

    
    @property
    def name(self):
        return self.__name

    # Kreiranje setera za svojstvo name
    @name.setter
    def name(self, new_name):
        self.__name = new_name

Da bismo testirali seter, možemo koristiti sledeći kod:

# Kreiranje novog psa
foo = Dog('foo')

# Promena imena psa
foo.name="bar"

# Štampanje novog imena psa na ekran
print("Novo ime psa je:", foo.name)

Pokretanje koda će proizvesti sledeći izlaz:

Novo ime psa je: bar

Značaj dekoratera u Python-u

Sada kada smo objasnili šta su dekorateri i videli neke primere, možemo da razgovaramo o tome zašto su dekorateri važni u Python-u. Dekorateri su važni iz više razloga, od kojih sam neke naveo u nastavku:

  • Omogućavaju ponovnu upotrebu koda: U gornjem primeru beleženja, mogli smo da koristimo @create_logger na bilo kojoj funkciji koju želimo. Ovo nam omogućava da dodamo funkcionalnost beleženja svim našim funkcijama bez ručnog pisanja za svaku funkciju.
  • Omogućavaju pisanje modularnog koda: Vraćajući se ponovo na primer beleženja, sa dekoraterima možete odvojiti osnovnu funkciju, u ovom slučaju say_hello, od druge funkcionalnosti koja vam je potrebna, u ovom slučaju beleženja.
  • Poboljšavaju framework-e i biblioteke: Dekorateri se u velikoj meri koriste u Python framework-ima i bibliotekama kako bi obezbedili dodatnu funkcionalnost. Na primer, u web framework-ima kao što su Flask ili Django, dekorateri se koriste za definisanje ruta, rukovanje autentifikacijom ili primenu middleware-a na određene view-e.

Završne reči

Dekorateri su neverovatno korisni; možete ih koristiti za proširenje funkcija bez promene njihove funkcionalnosti. Ovo je korisno kada želite da merite performanse funkcija, beležite svaki put kada se funkcija pozove, potvrdite argumente pre pozivanja funkcije ili proverite dozvole pre pokretanja funkcije. Jednom kada razumete dekoratere, moći ćete da pišete kod na čistiji način.

Sledeće, možda ćete želeti da pročitate naše članke o tuple-ovima i korišćenju cURL-a u Python-u.