lettura facile

I decoratori in Python

I decoratori in Python sono un concetto molto interessante che può sembrare complicato all'inizio, ma con un po' di pratica diventa una parte molto utile nella programmazione.

Cosa sono i decoratori? Un decoratore è una funzione che prende una funzione come input e restituisce una nuova funzione come output. In parole semplici, un decoratore è una funzione che modifica il comportamento di un'altra funzione o metodo. I decoratori ti permettono di "decorare" o arricchire le funzioni con funzionalità aggiuntive in modo elegante e leggibile.

Questo ti permette di aggiungere funzionalità a una funzione esistente senza modificarla direttamente.

Vediamo di capire cosa sono e come utilizzarli.

Crea un decoratore

In primo luogo devi creare un decoratore come una funzione.

def nome_decoratore(func):
    def wrapper():
        # Codice da eseguire prima della funzione
        func()
        # Codice da eseguire dopo la funzione
    return wrapper

Applica un decoratore a una funzione

Una volta definito, puoi aggiungere il decoratore a un'altra funzione scrivendo @nomedecoratore subito prima della definizione della funzione.

@nome_decoratore
def funzione()
   print("Questa è la mia funzione!")

Questa sintassi applica il decoratore alla funzione, modificandone il comportamento in base a quanto definito nel decoratore stesso.

In questo modo puoi aggiungere la stessa funzionalità a più funzioni, senza doverle modificare tutte. Inoltre, puoi aggiungere questa funzionalità a livello di codice solo quando ti serve. Inoltre, puoi usare i decoratori anche con le classi

Un esempio semplice

Immaginiamo di voler aggiungere un attributo 'lang' che stampa "english" quando viene chiamato.

def mydecorator(func):
    func.lang = 'english'
       return func

Ora definisci una funzione foo() anteponendo il decoratore con la sintassi @mydecorator.

@mydecorator
def foo():
    pass

In questo modo la funzione foo() viene decorata con l'attributo "lang".

Se ora chiami l'attributo "lang" dalla funzione foo(), questa restituisce "english".

print(foo.lang)

english

Puoi ottenere lo stesso risultato anche decorando la funzione con questa sintassi

foo = mydecorator(foo)

Il risultato finale è sempre lo stesso

print(foo.lang)

english

Puoi anche aggiungere più decoratori in cascata alla stessa funzione o metodo.

Ad esempio, crea due decoratori che aggiungono ognuno un attributo diverso "lang" e "version".

def mydecorator1(func):
     func.lang = 'english'
     return func

def mydecorator2(func):
     func.version = '3.1'
     return func

Poi definisci la funzione foo() anteponendo i due decoratori.

@mydecorator1
@mydecorator2
def foo():
    pass

La funzione è stata decorata con entrambi i decoratori.

In questo modo, puoi accedere agli attributi "lang" e "version" dalla stessa funzione.

print(foo.lang)
print(foo.version)

english
3.1

In questo caso, l'ordine dei decoratori è importante perché definisce la gerarchia di applicazione.

Ad esempio, crea due decoratori che definisco l'attributo "lang"

def mydecorator1(func):
     func.lang = 'italian'
     return func

def mydecorator2(func):
     func.lang = 'english'
     return func

Poi definisci la funzione foo() anteponendo due decoratori

@mydecorator1
@mydecorator2
def foo():
    pass

In questo caso, foo è decorato prima con mydecorator1 e poi con mydecorator2.

Se adesso accedi all'attributo "lang", Python restituisce il risultato "italian" perché trova l'attributo nel primo decoratore mydecorator1 e ignora quello nel secondo decoratore mydecorator2

print(foo.lang)

italian

Decoratori con parametri

A volte, può essere utile avere decoratori che accettano parametri. Questo aggiunge un livello di complessità, ma è molto potente.

Vediamo un esempio:

def repeat(num_times):
    def decorator_repeat(func):
       def wrapper(*args, **kwargs):
           for _ in range(num_times):
               result = func(*args, **kwargs)
           return result
       return wrapper
    return decorator_repeat

Ora puoi usare `repeat` con un parametro per ripetere la funzione più volte:

@repeat(num_times=3)
def greet(name):
    print(f"Hello, {name}!")

Quando chiami la funzione, questa viene eseguita per tre volte

greet("Bob")

Hello, Bob!
Hello, Bob!
Hello, Bob!

In alternativa puoi definire la funzione decorata usando questa sintassi

def greet(name):
    print(f"Hello, {name}!")

greet2=repeat(3)(greet)

In questo modo puoi dare alla funzione decorata un nome diverso dalla funzione originale.

greet2("Bob")

Hello, Bob!
Hello, Bob!
Hello, Bob!

Decoratori di classe

I decoratori possono essere usati anche con le classi. Un esempio comune è il decoratore `@staticmethod` o `@classmethod` in Python.

Ad esempio, crea un decoratore che aggiunge un metodo `greet` a una classe:

def add_greet_method(cls):
    def greet(self):
        print("Benvenuto!")
    cls.greet = greet
    return cls

Il decoratore add_greet_method() prende una classe `cls` come argomento e la modifica.

In questo esempio, il decoratore definisce un metodo greet() che stampa "Benvenuto!" e lo aggiunge alla classe `cls`.

Applica questo decoratore a una semplice classe `Person`:

@add_greet_method
class Person:
    def __init__(self, name):
        self.name = name

Crea un'istanza della classe Person

p = Person("Alice")

Infine, chiama il nuovo metodo greet

p.greet()

Benvenuto!

Si tratta di un esempio più semplice e pratico che spiega come usare i decoratori applicati alle classi:

Facciamo un altro esempio più complesso.

def add_class_method(cls):
     cls.class_method = classmethod(lambda cls: print(f"This is a class method in {cls.__name__}"))
     return cls

@add_class_method
class MyClass:
    pass

All'interno del decoratore, viene aggiunto un metodo di classe alla classe passata come argomento (cls).

Questo metodo di classe è definito utilizzando classmethod, che è una funzione built-in di Python che trasforma una funzione regolare in un metodo di classe.

In questo caso il metodo di classe è definito utilizzando una lambda function. Questa funzione stampa un messaggio che include il nome della classe (cls.__name__).

Dopo aver decorato MyClass, questa classe avrà un nuovo metodo di classe chiamato class_method

Ad esempio, accedi al metodo della classe

MyClass.class_method()

Questo stamperà:

This is a class method in MyClass

Come applicare un decoratore a una funzione senza modificarla

Spesso si utilizzano i decoratori per decorare le funzioni senza modificarle direttamente.

In questi casi si parla di funzione wrapper il decoratore è una sorta di involucro (wrapper) che racchiude la funzione da decorare e crea un'altra funzione decorata.

Ad esempio, definisci un decoratore che trasforma il risultato della funzione decorata nel suo valore assoluto:

def absolute_result(f):
    def wrapper(*args, **kwargs):
        result = f(*args, **kwargs)
        return abs(result)
    return wrapper

Poi definisci una funzione semplice che calcola la somma di due numeri senza applicagli il decoratore:

def add(a, b):
    """Calcola la somma di due numeri."""
    return a + b

Ora applica il decoratore senza modificare la funzione originale

decorated_add = absolute_result(add)

In questo modo puoi usare entrambe le funzioni a tua discrezione.

Ad esempio, puoi chiamare la funzione decorata che restituisce il valore assoluto della somma.

print(decorated_add(-5, 3))

2

Puoi chiamare anche la funzione originale, non decorata, che restituisce la somma algebrica.

print(add(-5, 3))

-2

Questo approccio ti permette di applicare un decoratore a una funzione senza modificarla direttamente, mantenendo intatta la funzione originale.

Ricorda che i metadati della funzione originale non sono preservati nella funzione wrapper.

Ad esempio, in questo esempio i metadati "decorated_add.__name__" e "decorated_add.__doc__" non sono preservati in memoria.

print(decorated_add.__name__)
print(decorated_add.__doc__)

wrapper
None

Puoi conoscerli solo richiamandoli direttamente dalla funzione originale.

print(add.__name__)
print(add.__doc__)

add
Calcola la somma di due numeri.

Se vuoi preservare i metadati della funzione oriignale devi seguire un approccio diverso, usando functools.wrapper.

Le funzioni wrapper functools.wrapper

Per preservare i metadati della funzione originale nella funzione wrapper puoi definirla usando il modulo functools.wrapper.

Ad esempio, questa funzione wrapper calcola il valore assoluto del risultato di un'altra funzione.

import functools

def absolute_result(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        result = f(*args, **kwargs)
        return abs(result)
    return wrapper

Definisci una funzione che calcola la somma di due numeri:

def add(a, b):
    """Calcola la somma di due numeri."""
    return a + b

Applica il decoratore e assegna la funzione decorata a una nuova variabile:

decorated_add = absolute_result(add)

Anche in questo caso puoi usare entrambe le funzioni, sia quella decorata che quella originale.

print(decorated_add(-5, -3))

8

Tuttavia, in questo caso i metadati della funzione originale sono preservati nella funzione decorata grazie all'uso di functools.wraps.

print(decorated_add.__name__)
print(decorated_add.__doc__)

add
Calcola la somma di due numeri.

Anche i metadati della funzione originale sono intatti.

print(add.__name__)
print(add.__doc__)

add
Calcola la somma di due numeri.

Questo approccio ti permette di applicare un decoratore a una funzione senza modificarla direttamente, mantenendo intatta la funzione originale e preservando i metadati della funzione decorata grazie a functools.wraps.

In conclusione, i decoratori in Python sono uno strumento molto utile per estendere e modificare il comportamento delle funzioni e dei metodi anche se inizialmente sono un po' complessi da capire.

Ricorda, la chiave per comprendere i decoratori è sapere che sono solo funzioni che prendono un'altra funzione come argomento e restituiscono una nuova funzione. Una volta capito questo, puoi creare decoratori sempre più complessi e utili per il tuo codice.




Se qualcosa non ti è chiaro, scrivi la tua domanda nei commenti.




FacebookTwitterLinkedinLinkedin