Spiegazione (con esempi e casi d’uso)

I decoratori Python sono un costrutto incredibilmente utile in Python. Usando i decoratori in Python, possiamo modificare il comportamento di una funzione avvolgendola all’interno di un’altra funzione. I decoratori ci consentono di scrivere codice più pulito e condividere funzionalità. Questo articolo è un tutorial non solo su come utilizzare i decoratori, ma anche su come crearli.

Conoscenze preliminari

L’argomento dei decoratori in Python richiede alcune conoscenze di base. Di seguito, ho elencato alcuni concetti che dovresti già conoscere per dare un senso a questo tutorial. Ho anche collegato risorse in cui puoi rispolverare i concetti se necessario.

Pitone di base

Questo argomento è un argomento più intermedio/avanzato. Di conseguenza, prima di tentare di imparare, dovresti già avere familiarità con le basi di Python, come tipi di dati, funzioni, oggetti e classi.

Dovresti anche comprendere alcuni concetti orientati agli oggetti come getter, setter e costruttori. Se non hai familiarità con il linguaggio di programmazione Python, ecco alcune risorse per iniziare.

Le funzioni sono cittadini di prima classe

Oltre a Python di base, dovresti anche essere consapevole di questo concetto più avanzato in Python. Le funzioni, e praticamente tutto il resto in Python, sono oggetti come int o string. Poiché sono oggetti, puoi fare alcune cose con loro, vale a dire:

  • Puoi passare una funzione come argomento a un’altra funzione nello stesso modo in cui passi una stringa o un int come argomento di una funzione.
  • Le funzioni possono anche essere restituite da altre funzioni come restituiresti altri valori stringa o int.
  • Le funzioni possono essere memorizzate in variabili

Infatti, l’unica differenza tra oggetti funzionali e altri oggetti è che gli oggetti funzionali contengono il metodo magico __call__().

Se tutto va bene, a questo punto, ti senti a tuo agio con la conoscenza dei prerequisiti. Possiamo iniziare a discutere l’argomento principale.

Che cos’è un decoratore Python?

Un decoratore Python è semplicemente una funzione che accetta una funzione come argomento e restituisce una versione modificata della funzione che è stata passata. In altre parole, la funzione foo è un decoratore se accetta, come argomento, la funzione bar e restituisce un’altra funzione baz.

La funzione baz è una modifica di bar nel senso che all’interno del corpo di baz c’è una chiamata alla funzione bar. Tuttavia, prima e dopo la chiamata al bar, baz può fare qualsiasi cosa. Quello era un boccone; ecco un codice per illustrare la situazione:

# Foo is a decorator, it takes in another function, bar as an argument
def foo(bar):

    # Here we create baz, a modified version of bar
    # baz will call bar but can do anything before and after the function call
    def baz():

        # Before calling bar, we print something
        print("Something")

        # Then we run bar by making a function call
        bar()

        # Then we print something else after running bar
        print("Something else")

    # Lastly, foo returns baz, a modified version of bar
    return baz

Come creare un decoratore in Python?

Per illustrare come i decoratori vengono creati e utilizzati in Python, lo illustrerò con un semplice esempio. In questo esempio, creeremo una funzione decoratore di logger che registrerà il nome della funzione che sta decorando ogni volta che la funzione viene eseguita.

Per iniziare, abbiamo creato la funzione decoratore. Il decoratore assume la funzione come argomento. func è la funzione che stiamo decorando.

def create_logger(func):
    # The function body goes here

All’interno della funzione decoratore, creeremo la nostra funzione modificata che registrerà il nome di func prima di eseguire func.

# Inside create_logger
def modified_func():
    print("Calling: ", func.__name__)
    func()

Successivamente, la funzione create_logger restituirà la funzione modificata. Di conseguenza, la nostra intera funzione create_logger sarà simile a questa:

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

    return modified_function

Abbiamo finito di creare il decoratore. La funzione create_logger è un semplice esempio di funzione decoratore. Accetta func, che è la funzione che stiamo decorando, e restituisce un’altra funzione, Modified_func. Modified_func prima registra il nome di func, prima di eseguire func.

Come usare i decoratori in Python

Per usare il nostro decoratore, usiamo la sintassi @ in questo modo:

@create_logger
def say_hello():
    print("Hello, World!")

Ora possiamo chiamare say_hello() nel nostro script e l’output dovrebbe essere il seguente testo:

Calling:  say_hello
"Hello, World"

Ma cosa sta facendo @create_logger? Bene, sta applicando il decoratore alla nostra funzione say_hello. Per capire meglio cosa sta facendo, il codice immediatamente sotto questo paragrafo otterrebbe lo stesso risultato dell’inserimento di @create_logger prima di say_hello.

def say_hello():
    print("Hello, World!")

say_hello = create_logger(say_hello)

In altre parole, un modo per usare i decoratori in Python è chiamare esplicitamente il decoratore passando la funzione come abbiamo fatto nel codice sopra. L’altro e più conciso modo è usare la sintassi @.

In questa sezione, abbiamo spiegato come creare decoratori Python.

Esempi leggermente più complicati

L’esempio precedente era un caso semplice. Ci sono esempi leggermente più complessi come quando la funzione che stiamo decorando accetta argomenti. Un’altra situazione più complicata è quando vuoi decorare un’intera classe. Tratterò qui entrambe queste situazioni.

Quando la funzione accetta argomenti

Quando la funzione che stai decorando accetta argomenti, la funzione modificata dovrebbe ricevere gli argomenti e passarli quando alla fine effettua la chiamata alla funzione non modificata. Se questo sembra confuso, lasciami spiegare in termini foo-bar.

Ricordiamo che foo è la funzione del decoratore, bar è la funzione che stiamo decorando e baz è la barra decorata. In tal caso, bar prenderà gli argomenti e li passerà a baz durante la chiamata a baz. Ecco un esempio di codice per consolidare il concetto:

def foo(bar):
    def baz(*args, **kwargs):
        # You can do something here
        ___
        # Then we make the call to bar, passing in args and kwargs
        bar(*args, **kwargs)
        # You can also do something here
        ___

    return baz

Se *args e **kwargs sembrano poco familiari; sono semplicemente puntatori rispettivamente agli argomenti posizionali e alla parola chiave.

È importante notare che baz ha accesso agli argomenti e può quindi eseguire alcune convalide degli argomenti prima di chiamare bar.

Un esempio potrebbe essere se avessimo una funzione decoratore, sure_string che assicurerebbe che l’argomento passato a una funzione che sta decorando sia una stringa; lo implementeremmo in questo modo:

def ensure_string(func):
    def decorated_func(text):
        if type(text) is not str:
             raise TypeError('argument to ' + func.__name__ + ' must be a string.')
        else:
             func(text)

    return decorated_func

Potremmo decorare la funzione say_hello in questo modo:

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

Quindi potremmo testare il codice usando questo:

say_hello('John') # Should run just fine
say_hello(3) # Should throw an exception

E dovrebbe produrre il seguente output:

Hello 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('argument to + func._name_ + must be a string.')
TypeError: argument to say hello must be a string. $0

Come previsto, lo script è riuscito a stampare “Hello John” perché “John” è una stringa. Ha generato un’eccezione durante il tentativo di stampare “Ciao 3” perché “3” non era una stringa. Il decoratore sure_string potrebbe essere utilizzato per convalidare gli argomenti di qualsiasi funzione che richieda una stringa.

Decorare una classe

Oltre alle sole funzioni di decorazione, possiamo anche decorare le classi. Quando aggiungi un decoratore a una classe, il metodo decorato sostituisce il metodo costruttore/iniziatore della classe (__init__).

Tornando a foo-bar, supponiamo che foo sia il nostro decoratore e Bar sia la classe che stiamo decorando, quindi foo decorerà Bar.__init__. Questo sarà utile se vogliamo fare qualcosa prima che gli oggetti del tipo Bar vengano istanziati.

Ciò significa che il seguente codice

def foo(func):
    def new_func(*args, **kwargs):
        print('Doing some stuff before instantiation')
        func(*args, **kwargs)

    return new_func

@foo
class Bar:
    def __init__(self):
        print("In initiator")

È equivalente a

def foo(func):
    def new_func(*args, **kwargs):
        print('Doing some stuff before instantiation')
        func(*args, **kwargs)

    return new_func

class Bar:
    def __init__(self):
        print("In initiator")


Bar.__init__ = foo(Bar.__init__)

Infatti, istanziare un oggetto di classe Bar, definito utilizzando uno dei due metodi, dovrebbe darti lo stesso output:

Doing some stuff before instantiation
In initiator

Esempio Decoratori in Python

Mentre puoi definire i tuoi decoratori, ce ne sono alcuni che sono già integrati in Python. Ecco alcuni dei decoratori comuni che potresti incontrare in Python:

@staticmethod

Il metodo statico viene utilizzato su una classe per indicare che il metodo che sta decorando è un metodo statico. I metodi statici sono metodi che possono essere eseguiti senza la necessità di creare un’istanza della classe. Nell’esempio di codice seguente creiamo una classe Dog con un metodo statico bark.

class Dog:
    @staticmethod
    def bark():
        print('Woof, woof!')

Ora è possibile accedere al metodo bark in questo modo:

Dog.bark()

E l’esecuzione del codice produrrebbe il seguente output:

Woof, woof!

Come ho accennato nella sezione Come usare i decoratori, i decoratori possono essere usati in due modi. La sintassi @ è la più concisa essendo una delle due. L’altro metodo consiste nel chiamare la funzione decoratore, passando la funzione che vogliamo decorare come argomento. Significa che il codice sopra raggiunge la stessa cosa del codice qui sotto:

class Dog:
    def bark():
        print('Woof, woof!')

Dog.bark = staticmethod(Dog.bark)

E possiamo ancora usare il metodo della corteccia allo stesso modo

Dog.bark()

E produrrebbe lo stesso output

Woof, woof!

Come puoi vedere, il primo metodo è più pulito ed è più ovvio che la funzione è una funzione statica prima ancora che tu abbia iniziato a leggere il codice. Di conseguenza, per gli esempi rimanenti, utilizzerò il primo metodo. Ma ricorda solo che il secondo metodo è un’alternativa.

@classmethod

Questo decoratore viene utilizzato per indicare che il metodo che sta decorando è un metodo di classe. I metodi di classe sono simili ai metodi statici in quanto entrambi non richiedono che la classe venga istanziata prima di poter essere chiamati.

Tuttavia, la differenza principale è che i metodi di classe hanno accesso agli attributi di classe mentre i metodi statici no. Questo perché Python passa automaticamente la classe come primo argomento a un metodo di classe ogni volta che viene chiamato. Per creare un metodo di classe in Python, possiamo usare il decoratore di metodi di classe.

class Dog:
    @classmethod
    def what_are_you(cls):
        print("I am a " + cls.__name__ + "!")

Per eseguire il codice, chiamiamo semplicemente il metodo senza istanziare la classe:

Dog.what_are_you()

E l’output è:

I am a Dog!

@proprietà

Il decoratore di proprietà viene utilizzato per etichettare un metodo come setter di proprietà. Tornando al nostro esempio Dog, creiamo un metodo che recuperi il nome del Dog.

class Dog:
    # Creating a constructor method that takes in the dog's name
    def __init__(self, name):

         # Creating a private property name
         # The double underscores make the attribute private
         self.__name = name

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

Ora possiamo accedere al nome del cane come una normale proprietà,

# Creating an instance of the class
foo = Dog('foo')

# Accessing the name property
print("The dog's name is:", foo.name)

E il risultato dell’esecuzione del codice sarebbe

The dog's name is: foo

@property.setter

Il decoratore property.setter viene utilizzato per creare un metodo setter per le nostre proprietà. Per utilizzare il decoratore @property.setter, sostituisci property con il nome della proprietà per cui stai creando un setter. Ad esempio, se stai creando un setter per il metodo per la proprietà foo, il tuo decoratore sarà @foo.setter. Ecco un esempio di cane per illustrare:

class Dog:
    # Creating a constructor method that takes in the dog's name
    def __init__(self, name):

         # Creating a private property name
         # The double underscores make the attribute private
         self.__name = name

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

    # Creating a setter for our name property
    @name.setter
    def name(self, new_name):
        self.__name = new_name

Per testare il setter, possiamo usare il seguente codice:

# Creating a new dog
foo = Dog('foo')

# Changing the dog's name
foo.name="bar"

# Printing the dog's name to the screen
print("The dog's new name is:", foo.name)

L’esecuzione del codice produrrà il seguente output:

The dogs's new name is: bar

Importanza dei decoratori in Python

Ora che abbiamo visto cosa sono i decoratori e avete visto alcuni esempi di decoratori, possiamo discutere perché i decoratori sono importanti in Python. I decoratori sono importanti per diversi motivi. Alcuni di loro, ho elencato di seguito:

  • Consentono la riusabilità del codice: nell’esempio di registrazione sopra riportato, potremmo utilizzare @create_logger su qualsiasi funzione desideriamo. Questo ci consente di aggiungere funzionalità di registrazione a tutte le nostre funzioni senza scriverla manualmente per ciascuna funzione.
  • Ti consentono di scrivere codice modulare: ancora una volta, tornando all’esempio di registrazione, con i decoratori, puoi separare la funzione principale, in questo caso say_hello dall’altra funzionalità di cui hai bisogno, in questo caso la registrazione.
  • Migliorano framework e librerie: i decoratori sono ampiamente utilizzati nei framework e nelle librerie Python per fornire funzionalità aggiuntive. Ad esempio, in framework Web come Flask o Django, i decoratori vengono utilizzati per definire percorsi, gestire l’autenticazione o applicare middleware a viste specifiche.

Parole finali

I decoratori sono incredibilmente utili; puoi usarli per estendere le funzioni senza alterarne la funzionalità. Ciò è utile quando si desidera cronometrare l’esecuzione delle funzioni, registrare ogni volta che viene chiamata una funzione, convalidare gli argomenti prima di chiamare una funzione o verificare le autorizzazioni prima che una funzione venga eseguita. Una volta capiti i decoratori, sarai in grado di scrivere codice in modo più pulito.

Successivamente, potresti voler leggere i nostri articoli sulle tuple e sull’uso di cURL in Python.