Explicat (cu exemple și cazuri de utilizare)

În limbajul Python, decoratorii reprezintă o facilitate extrem de valoroasă. Aceștia ne permit să modificăm comportamentul unei funcții prin încapsularea ei într-o altă funcție. Practic, decoratorii ne ajută să scriem cod mai ordonat și să reutilizăm funcționalități. Acest articol este conceput ca un ghid detaliat, care te va învăța nu doar cum să folosești decoratorii, ci și cum să îi creezi.

Cunoștințe Necesare

Pentru a înțelege pe deplin conceptul de decoratori în Python, este necesar să ai deja o bază solidă de cunoștințe. Mai jos, am alcătuit o listă cu câteva noțiuni fundamentale pe care ar trebui să le cunoști înainte de a continua cu acest tutorial. De asemenea, am inclus link-uri către resurse suplimentare, în cazul în care dorești să îți reîmprospătezi cunoștințele.

Noțiuni de Bază Python

Acest subiect se încadrează într-o categorie intermediară spre avansată. Prin urmare, este esențial să fii familiarizat cu elementele de bază ale limbajului Python, cum ar fi tipurile de date, funcțiile, obiectele și clasele. De asemenea, ar trebui să înțelegi concepte legate de programarea orientată pe obiecte, cum ar fi getteri, setteri și constructori. Dacă nu ai experiență cu limbajul Python, iată câteva resurse care te pot ajuta să începi.

Funcțiile ca Obiecte de Primă Clasă

Pe lângă noțiunile de bază, ar trebui să fii familiarizat cu acest concept mai avansat din Python. Funcțiile, la fel ca aproape orice altceva în Python, sunt considerate obiecte, similar cu int sau string. Deoarece sunt obiecte, le poți utiliza în diverse moduri:

  • Poți transmite o funcție ca argument unei alte funcții, în același mod în care transmiți un șir de caractere sau un întreg.
  • Funcțiile pot fi returnate de alte funcții, similar cu returnarea oricărei alte valori.
  • Funcțiile pot fi stocate în variabile.

De fapt, singura distincție între obiectele funcționale și celelalte obiecte constă în faptul că obiectele funcționale dețin metoda magică __call__().

Sperăm că acum te simți confortabil cu cunoștințele preliminare. Putem trece la discuția despre subiectul principal.

Ce Este un Decorator în Python?

Un decorator în Python este, în esență, o funcție care primește ca argument o altă funcție și returnează o versiune modificată a acesteia. Cu alte cuvinte, o funcție numită „foo” este considerată un decorator dacă primește ca argument o funcție „bar” și returnează o altă funcție numită „baz”.

Funcția „baz” reprezintă o modificare a funcției „bar”, în sensul că în interiorul funcției „baz” există un apel la funcția „bar”. Cu toate acestea, înainte și după apelul către „bar”, funcția „baz” poate executa orice fel de operație. Pentru a clarifica situația, iată un exemplu de cod:

# Foo este un decorator, primește o altă funcție, bar, ca argument
def foo(bar):

    # Aici creăm baz, o versiune modificată a lui bar
    # baz va apela bar, dar poate face orice înainte și după apelul funcției
    def baz():

        # Înainte de a apela bar, afișăm ceva
        print("Ceva")

        # Apoi executăm bar prin apelarea funcției
        bar()

        # Apoi afișăm altceva după executarea lui bar
        print("Altceva")

    # În final, foo returnează baz, o versiune modificată a lui bar
    return baz

Cum Se Creează un Decorator în Python?

Pentru a ilustra procesul de creare și utilizare a decoratorilor în Python, vom folosi un exemplu simplu. Vom crea o funcție decorator numită „logger” care va înregistra numele funcției decorate de fiecare dată când aceasta este apelată.

Începem prin a defini funcția decorator. Aceasta primește o funcție ca argument, pe care o vom numi „func”. Aceasta este funcția pe care o vom decora.

def create_logger(func):
    # Corpul funcției va fi completat aici

În interiorul funcției decorator, vom defini funcția noastră modificată care va înregistra numele funcției înainte de a o executa.

# În interiorul funcției create_logger
def modified_func():
    print("Apelăm: ", func.__name__)
    func()

Apoi, funcția „create_logger” va returna funcția modificată. Prin urmare, funcția noastră „create_logger” completă va arăta astfel:

def create_logger(func):
    def modified_func():
        print("Apelăm: ", func.__name__)
        func()

    return modified_func

Am finalizat crearea decoratorului. Funcția „create_logger” este un exemplu simplu de funcție decorator. Ea primește ca argument funcția pe care o decorăm și returnează o altă funcție, numită „modified_func”. Aceasta din urmă înregistrează numele funcției inițiale, înainte de a o executa.

Cum Să Folosești Decoratorii în Python

Pentru a utiliza decoratorul nostru, folosim sintaxa „@” astfel:

@create_logger
def say_hello():
    print("Salut, Lume!")

Acum, putem apela „say_hello()” în scriptul nostru, iar rezultatul ar trebui să fie următorul:

Apelăm:  say_hello
Salut, Lume!

Dar ce face „@create_logger”? Ei bine, acesta aplică decoratorul funcției noastre „say_hello”. Pentru a înțelege mai bine ce se întâmplă, codul de mai jos ar avea același efect ca și plasarea „@create_logger” înaintea funcției „say_hello”.

def say_hello():
    print("Salut, Lume!")

say_hello = create_logger(say_hello)

Cu alte cuvinte, o modalitate de a utiliza decoratorii în Python este de a apela explicit decoratorul, transmițând funcția, așa cum am făcut în codul de mai sus. O altă modalitate, mai concisă, este să folosim sintaxa „@”.

În această secțiune, am analizat cum să creăm decoratori în Python.

Exemple Mai Complexe

Exemplul de mai sus a fost unul simplu. Există și situații mai complicate, cum ar fi atunci când funcția pe care o decorăm primește argumente sau când vrem să decorăm o întreagă clasă. Vom discuta ambele situații în continuare.

Când Funcția Primește Argumente

Când funcția pe care o decorați primește argumente, funcția modificată trebuie să primească acele argumente și să le transmită atunci când apelează funcția nemodificată. Pentru a clarifica, vom folosi din nou analogia „foo-bar”.

Amintește-ți că „foo” este funcția decorator, „bar” este funcția pe care o decorăm și „baz” este funcția „bar” decorată. În acest caz, „bar” va primi argumentele și le va transmite lui „baz” în timpul apelului. Iată un exemplu de cod pentru a consolida conceptul:

def foo(bar):
    def baz(*args, **kwargs):
        # Poți face ceva aici
        ___
        # Apoi apelăm bar, transmițând argumentele args și kwargs
        bar(*args, **kwargs)
        # Poți face, de asemenea, ceva aici
        ___

    return baz

Dacă *args și **kwargs îți sună necunoscute, ele sunt, simplu spus, indicatori pentru argumentele poziționale și argumentele cheie, respectiv.

Este important de menționat că funcția „baz” are acces la argumente și, prin urmare, poate efectua validarea argumentelor înainte de a apela funcția „bar”.

Un exemplu ar fi funcția decorator „assure_string”, care se asigură că argumentul transmis unei funcții decorate este un șir de caractere. Aceasta ar fi implementată astfel:

def ensure_string(func):
    def decorated_func(text):
        if type(text) is not str:
             raise TypeError('argumentul pentru ' + func.__name__ + ' trebuie să fie un șir de caractere.')
        else:
             func(text)

    return decorated_func

Am putea decora funcția „say_hello” astfel:

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

Apoi, am putea testa codul folosind:

say_hello('John') # Ar trebui să funcționeze
say_hello(3) # Ar trebui să genereze o excepție

Și ar trebui să producă următorul rezultat:

Salut John
Traceback (most recent call last):
   File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in <module> say hello(3) # should throw an exception
   File "/home/anesu/Documents/python-tu$ ./decorators.pytorial/./decorators.py", line 7, in decorated_func raise TypeError('argumentul pentru + func._name_ + trebuie să fie un șir de caractere.')
TypeError: argumentul pentru say hello trebuie să fie un șir de caractere. $0

După cum era de așteptat, scriptul a reușit să afișeze „Salut John”, deoarece „John” este un șir de caractere. A generat o excepție când a încercat să afișeze „Salut 3”, deoarece „3” nu este un șir de caractere. Decoratorul „assure_string” poate fi utilizat pentru a valida argumentele oricărei funcții care necesită un șir de caractere.

Decorarea unei Clase

Pe lângă decorarea funcțiilor, putem decora și clase. Când adăugăm un decorator unei clase, metoda decorată înlocuiește metoda constructor/inițiator a clasei (__init__).

Revenind la „foo-bar”, să presupunem că „foo” este decoratorul și „Bar” este clasa pe care o decorăm. Atunci, „foo” va decora „Bar.__init__”. Acest lucru este util dacă dorim să efectuăm operații înainte ca obiectele de tip „Bar” să fie instanțiate.

Aceasta înseamnă că următorul cod

def foo(func):
    def new_func(*args, **kwargs):
        print('Facem niște lucruri înainte de instanțiere')
        func(*args, **kwargs)

    return new_func

@foo
class Bar:
    def __init__(self):
        print("În inițiator")

Este echivalent cu:

def foo(func):
    def new_func(*args, **kwargs):
        print('Facem niște lucruri înainte de instanțiere')
        func(*args, **kwargs)

    return new_func

class Bar:
    def __init__(self):
        print("În inițiator")


Bar.__init__ = foo(Bar.__init__)

De fapt, instanțierea unui obiect din clasa „Bar”, definită folosind oricare dintre cele două metode, ar trebui să afișeze același rezultat:

Facem niște lucruri înainte de instanțiere
În inițiator

Exemple de Decoratori în Python

Deși poți defini propriii decoratori, există unii care sunt deja integrați în Python. Iată câțiva dintre cei mai comuni decoratori pe care îi poți întâlni:

@staticmethod

Decoratorul „staticmethod” este folosit pentru a indica faptul că metoda pe care o decorează este o metodă statică. Metodele statice sunt metode care pot fi apelate fără a fi nevoie de instanțierea clasei. În următorul exemplu, creăm o clasă „Dog” cu o metodă statică „bark”.

class Dog:
    @staticmethod
    def bark():
        print('Ham, ham!')

Acum, metoda „bark” poate fi accesată astfel:

Dog.bark()

Și execuția codului ar produce următorul rezultat:

Ham, ham!

După cum am menționat în secțiunea despre utilizarea decoratorilor, aceștia pot fi folosiți în două moduri. Sintaxa „@” este una mai concisă, fiind una dintre ele. Cealaltă metodă este să apelăm funcția decorator, transmițând funcția pe care dorim să o decorăm ca argument. Adică, codul de mai sus face același lucru ca și codul de mai jos:

class Dog:
    def bark():
        print('Ham, ham!')

Dog.bark = staticmethod(Dog.bark)

Și putem folosi în continuare metoda „bark” în același mod:

Dog.bark()

Și ar produce același rezultat:

Ham, ham!

După cum se poate vedea, prima metodă este mai curată și mai evidentă că funcția este o metodă statică chiar înainte de a începe să citești codul. Ca urmare, în exemplele rămase, voi folosi prima metodă. Dar rețineți că a doua metodă este o alternativă.

@classmethod

Acest decorator este folosit pentru a indica faptul că metoda pe care o decorează este o metodă de clasă. Metodele de clasă sunt similare cu metodele statice, deoarece ambele nu necesită instanțierea clasei înainte de a putea fi apelate.

Cu toate acestea, principala diferență este că metodele de clasă au acces la atributele clasei, în timp ce metodele statice nu. Acest lucru se datorează faptului că Python transmite automat clasa ca prim argument unei metode de clasă ori de câte ori este apelată. Pentru a crea o metodă de clasă în Python, putem folosi decoratorul „classmethod”.

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

Pentru a rula codul, apelăm pur și simplu metoda fără a instanția clasa:

Dog.what_are_you()

Și rezultatul este:

Sunt un Dog!

@property

Decoratorul „property” este folosit pentru a eticheta o metodă ca fiind un getter de proprietate. Revenind la exemplul nostru cu câinele, vom crea o metodă care primește numele câinelui.

class Dog:
    # Crearea unei metode constructor care primește numele câinelui
    def __init__(self, name):

         # Crearea unei proprietăți private denumite name
         # Underscore-urile duble fac atributul privat
         self.__name = name

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

Acum, putem accesa numele câinelui ca pe o proprietate normală:

# Crearea unei instanțe a clasei
foo = Dog('foo')

# Accesarea proprietății name
print("Numele câinelui este:", foo.name)

Iar rezultatul execuției codului va fi:

Numele câinelui este: foo

@property.setter

Decoratorul „property.setter” este utilizat pentru a crea o metodă setter pentru proprietățile noastre. Pentru a utiliza decoratorul „@property.setter”, înlocuiți „property” cu numele proprietății pentru care creați un setter. De exemplu, dacă creați un setter pentru metoda proprietății „foo”, decoratorul va fi „@foo.setter”. Iată un exemplu cu câinele pentru a ilustra:

class Dog:
    # Crearea unei metode constructor care primește numele câinelui
    def __init__(self, name):

         # Crearea unei proprietăți private denumite name
         # Underscore-urile duble fac atributul privat
         self.__name = name

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

    # Crearea unui setter pentru proprietatea name
    @name.setter
    def name(self, new_name):
        self.__name = new_name

Pentru a testa setter-ul, putem folosi următorul cod:

# Crearea unui nou câine
foo = Dog('foo')

# Schimbarea numelui câinelui
foo.name="bar"

# Afișarea numelui noului câine
print("Noul nume al câinelui este:", foo.name)

Execuția codului va produce următorul rezultat:

Noul nume al câinelui este: bar

Importanța Decoratorilor în Python

Acum că am analizat ce sunt decoratorii și am văzut câteva exemple, putem discuta de ce sunt importanți în Python. Aceștia sunt valoroși din mai multe motive. Câteva dintre ele sunt enumerate mai jos:

  • Permit reutilizarea codului: în exemplul cu înregistrarea, am putea folosi „@create_logger” pentru orice funcție dorim. Acest lucru ne permite să adăugăm funcționalitatea de înregistrare la toate funcțiile noastre fără a o scrie manual pentru fiecare.
  • Permit scrierea unui cod modular: Din nou, referindu-ne la exemplul de înregistrare, cu decoratorii, poți separa funcția de bază, în acest caz „say_hello”, de cealaltă funcționalitate de care ai nevoie, cum ar fi înregistrarea.
  • Îmbunătățesc framework-urile și bibliotecile: Decoratorii sunt folosiți pe scară largă în framework-urile și bibliotecile Python pentru a oferi funcționalități suplimentare. De exemplu, în framework-uri web precum Flask sau Django, decoratorii sunt folosiți pentru a defini rutele, a gestiona autentificarea sau a aplica middleware anumitor vizualizări.

Cuvinte de Încheiere

Decoratorii sunt extrem de utili; îi poți folosi pentru a extinde funcțiile fără a le modifica funcționalitatea de bază. Acest lucru este util atunci când dorești să măsori performanța funcțiilor, să înregistrezi de fiecare dată când este apelată o funcție, să validezi argumentele înainte de a apela o funcție sau să verifici permisiunile înainte de executarea unei funcții. Odată ce înțelegi decoratorii, vei putea scrie codul într-un mod mai curat.

Apoi, s-ar putea să vrei să citești articolele noastre despre tupluri și utilizarea cURL în Python.