🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Decoratori in Python

Codegrind TeamSep 11 2024

I decoratori in Python sono una potente funzionalità che consente di modificare o estendere il comportamento di funzioni o classi senza modificarne direttamente il codice sorgente. I decoratori sono ampiamente utilizzati in molte librerie Python e offrono una sintassi semplice per applicare modifiche a funzioni, classi o metodi.

Cosa sono i Decoratori?

Un decoratore è una funzione che accetta un’altra funzione come argomento e restituisce una nuova funzione, tipicamente modificata o estesa. Questo consente di aggiungere funzionalità a una funzione esistente in modo flessibile.

Sintassi di un Decoratore

Il decoratore si applica utilizzando la @ seguita dal nome del decoratore sopra la definizione della funzione che si desidera decorare. Ecco un esempio semplice:

def decoratore_di_esempio(funzione):
    def wrapper():
        print("Prima della funzione")
        funzione()
        print("Dopo la funzione")
    return wrapper

@decoratore_di_esempio
def funzione_esempio():
    print("Funzione principale")

funzione_esempio()

Output:

Prima della funzione
Funzione principale
Dopo la funzione

Nel codice sopra:

  • decoratore_di_esempio è un decoratore che avvolge la funzione funzione_esempio.
  • La funzione originale è incapsulata all’interno di wrapper(), che aggiunge un comportamento prima e dopo l’esecuzione della funzione principale.

Creazione di un Decoratore

Vediamo come creare un decoratore passo dopo passo. Un decoratore classico accetta una funzione come argomento, definisce un wrapper interno che racchiude il comportamento aggiuntivo e restituisce il wrapper.

Esempio: Decoratore che misura il tempo di esecuzione

Un esempio pratico di decoratore è uno che misura quanto tempo impiega una funzione per essere eseguita.

import time

def calcola_tempo(funzione):
    def wrapper(*args, **kwargs):
        inizio = time.time()
        risultato = funzione(*args, **kwargs)
        fine = time.time()
        print(f"Tempo di esecuzione: {fine - inizio:.4f} secondi")
        return risultato
    return wrapper

@calcola_tempo
def somma_numeri(a, b):
    time.sleep(1)  # Simula un'operazione lunga 1 secondo
    return a + b

print(somma_numeri(5, 10))

Output:

Tempo di esecuzione: 1.0001 secondi
15

In questo esempio:

  • Il decoratore calcola_tempo misura quanto tempo ci vuole per eseguire la funzione somma_numeri.
  • Grazie a @calcola_tempo, la funzione somma_numeri viene automaticamente “decorata” con il codice per il calcolo del tempo.

Passare Argomenti ai Decoratori

I decoratori possono lavorare con funzioni che accettano argomenti utilizzando *args e **kwargs. Questo consente al decoratore di essere flessibile e applicabile a qualsiasi funzione, indipendentemente dal numero di parametri.

Ecco un esempio di un decoratore che logga gli argomenti passati a una funzione:

def log_args(funzione):
    def wrapper(*args, **kwargs):
        print(f"Chiamata a {funzione.__name__} con argomenti: {args}, {kwargs}")
        return funzione(*args, **kwargs)
    return wrapper

@log_args
def moltiplica(a, b):
    return a * b

print(moltiplica(3, 4))

Output:

Chiamata a moltiplica con argomenti: (3, 4), {}
12

Decoratori Multipli

È possibile applicare più decoratori a una singola funzione. La sintassi prevede la sovrapposizione dei decoratori uno sopra l’altro.

Ecco un esempio:

def decoratore_a(funzione):
    def wrapper(*args, **kwargs):
        print("Decoratore A inizia")
        risultato = funzione(*args, **kwargs)
        print("Decoratore A finisce")
        return risultato
    return wrapper

def decoratore_b(funzione):
    def wrapper(*args, **kwargs):
        print("Decoratore B inizia")
        risultato = funzione(*args, **kwargs)
        print("Decoratore B finisce")
        return risultato
    return wrapper

@decoratore_a
@decoratore_b
def funzione_prova():
    print("Funzione principale")

funzione_prova()

Output:

Decoratore A inizia
Decoratore B inizia
Funzione principale
Decoratore B finisce
Decoratore A finisce

In questo caso:

  • I decoratori vengono applicati dall’alto verso il basso. Quindi decoratore_a avvolge decoratore_b, che a sua volta avvolge la funzione funzione_prova.

Decoratori per Classi

I decoratori non si applicano solo alle funzioni, ma possono anche decorare le classi. Un decoratore di classe può modificare o estendere il comportamento di un’intera classe.

Ecco un esempio di decoratore che aggiunge un metodo a una classe:

def aggiungi_metodo(cls):
    cls.saluta = lambda self: print(f"Ciao, sono {self.nome}!")
    return cls

@aggiungi_metodo
class Persona:
    def __init__(self, nome):
        self.nome = nome

p = Persona("Marco")
p.saluta()  # Output: Ciao, sono Marco!

In questo esempio:

  • Il decoratore aggiungi_metodo aggiunge dinamicamente un nuovo metodo saluta alla classe Persona.

Decoratori con Parametri

In alcuni casi, potresti voler passare dei parametri a un decoratore. In questo caso, è necessario creare un decoratore annidato, che genera un decoratore effettivo.

Ecco un esempio:

def ripeti(n):
    def decoratore(funzione):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                funzione(*args, **kwargs)
        return wrapper
    return decoratore

@ripeti(3)
def saluta():
    print("Ciao!")

saluta()

Output:

Ciao!
Ciao!
Ciao!

Il decoratore ripeti prende un argomento n che specifica quante volte eseguire la funzione decorata.

Vantaggi dell’Utilizzo dei Decoratori

  • Riutilizzo del codice: I decoratori permettono di incapsulare funzionalità comuni (ad esempio logging, controllo degli accessi, caching) e applicarle a diverse funzioni o classi senza duplicare il codice.
  • Modularità: Separano la logica accessoria (come la misurazione del tempo o il logging) dal comportamento principale della funzione.
  • Leggibilità: I decoratori possono rendere il codice più leggibile, poiché la loro applicazione è immediatamente evidente dalla sintassi.

Considerazioni Finali

I decoratori sono uno strumento potente che consente di estendere o modificare il comportamento delle funzioni e delle classi in modo modulare e flessibile. Con i decoratori, è possibile separare la logica principale del programma da quella accessoria, mantenendo il codice più pulito e riutilizzabile.