Nuova sezione libri disponibile!

Python Decorators

Ludovico Russo

lettura in 6 minuti

Python è un linguaggio di programmazione molto pragmatico e potente. Tra le funzionalità che preferisco (anche se ho iniziato a sfruttare seriamente da poco), troviamo i decoratos (decoratori), a cui Python dedica una sintassi speciale.

In questo post, voglio analizzare l'utilizzo dei decoratori in Python, facendo vedere come possono essere usati per semplificare non poco lo sviluppo di API in un'applicazione Flask con un semplice esempio pratico.

Decorator Design Pattern

I decorator fanno parte dei Design Pattern fondamentali. Per chi non lo sapesse, un design pattern è una soluzione standardizzata (e spesso elegante) ad un problema ricorrente in programmazione.

Il decorator risolve la necessità (ai programmatori) di aggingere funzionalità ad uno specifico oggetto (per semplicità limitiamoci a parlare di funzioni), senza dover modificare l'oggetto stesso.

Facciamo un esempio pratico:

Supponiamo di definire una serie di funzioni (per semplicità due) che generano dei dizionari Python.

def dict_name(name):
    return dict(name=name)

def dict_age(age):
    return dict(age=age)

Queste due semplicissime funzioni ritornano due dizionari con un solo elemento entrambi:

>>> dict_name('ludovico')
{'name': 'ludovico'}
>>> dict_age(12)
{'age': 12}

Supponiamo a questo punto di voler utilizzare queste funzioni per ritornare non un oggetto dict ma un oggetto JSON, magari da utilizzare all'interno di un server web.

Le soluzioni possono essere due:

  1. Modificare le due funzioni come segue:
import json

def dict_name_json(name):
    return json.dumps(dict(name=name))

def dict_age_json(age):
    return json.dumps(dict(age=age))

Che può essere utilizzata esattamente nello stesso modo di prima:

>>> dict_age_json(12)
'{"age": 12}'
>>> dict_name_json('ludovico')
'{"name": "ludovico"}'
  1. Utilizzare la funzione json.dumps() esternamente alle due funzioni.
>>> json.dumps(dict_name('ludovico'))
'{"name": "ludovico"}'
>>> json.dumps(dict_age(12))
'{"age": 12}'

Entrambe queste soluzioni sono funzionali ma poco Pythoniche. Inoltre, richiedono al programma o di modificare una funziona esistente oppure di utilizzare continuamente la funzione json.dumps().

A questo punto ci viene in aiuto il pattern decorator. Un decorator è (essenzialmente) una funzione che prende in ingresso un'altra funzione e ne ritorna una simile ma con il comportamento modificato.

Riprendiamo il nostro esempio: se vogliamo utilizzare questo design pattern, dobbiamo scrivere una funzione che prende in ingresso un'altra funzione che ritorna un dizionario, e genera una funzione che ritorna lo stesso dizionario jsonizzato.

Ecco il codice:

def as_json(func):
    def func_wrapper(arg):
        d = func(arg)
        return json.dumps(d)
    return func_wrapper

Vediamo cosa fa questo codice:

  1. Per prima cosa abbiamo definito una funzione chiamata as_json, che prende come parametro una funzione generica func.

  2. All'interno di questa funzione, definiamo una seconda funzione chiamata func_wrapper. Questa funzione prende in ingresso un parametro (args), chiama la funzione func su args, e ritorna il risultato come json, utilizzando la funzione json.dumps().

  3. Alla fine, ritorniamo la funzione func_wrapper, che decora la funzione func.

Di seguito, vediamo come utilizzarla.

>>> dict_name = as_json(dict_name)
>>> dict_name('ludovico')
'{"name": "ludovico"}'
>>> dict_age = as_json(dict_age)
>>> dict_name(12)
'{"age": 12}'

In particolare, le due seguenti righe di codice

dict_name = as_json(dict_name)
dict_age = as_json(dict_age)

sono quelle in cui decoriamo le funzioni con il decoratore as_json.

Questa sintassi è un po' brutta, per questo motivo, python mette a disposizione una speciale sintassi che permette di decorare una funzione nel momento in cui viene creata:

@as_json
def dict_name(name):
    return dict(name=name)

@as_json
def dict_age(age):
    return dict(age=age)

Che è equivalente (ma decisamente più bella) a

def dict_name(name):
    return dict(name=name)
dict_name=as_json(dict_name)

def dict_age(age):
    return dict(age=age)
dict_age=as_json(dict_age)

Miglioriamo il decoratore

Notare che, a questo punto, il nostro decoratore funziona con qualsiasi funzione che ritorna un oggetto dict (o comunque un oggetto jsonizzabile) e ha un solo parametro in ingresso.

Che succede se proviamo ad usare questo decoratore con una funzione più complessa? Provando a lanciare il seguente codice

@as_json
def dict_name_age(name, age):
    return dict(name=name, age=age)

dict_name_age("ludovico", 12)

otterremo un errore

Traceback (most recent call last):
  File "decorator_example.py", line 13, in <module>
    print(dict_name_age("ludovico", 12))
TypeError: func_wrapper() takes 1 positional argument but 2 were given

Questo succede perchè la funzione func_wrap all'interno del decoratore prende solo un parametro. Come facciamo a fare in modo che questa prenda un numero indefinito di parametri in modo da essere compabile con qualsiasi funzione decorata? Ci aiuta una funzione di python chiamata unpacking, che magari discuterò in un altro post. Al momento, vi basti sapere che la soluzione è la seguente:

def as_json(func):
    def func_wrapper(*args, **kargs):
        d = func(*args, **kargs)
        return json.dumps(d)
    return func_wrapper

In cui *args, **kargs intercettano genericamente qualsiasi parametro venga passato alla func_wrapper e li passano alla funzione func decorata. Ora possiamo usare il nostro decoratore in modo più generale.

@as_json
def dict_name_age(name, age):
    return dict(name=name, age=age)
>>> dict_name_age("ludovico", 12)
'{"name": "ludovico", "age": 12}'

Decoratori parametrizzabili

A questo punto le cose potrebbero sembrare più complesse, ma in realtà una volta capito il meccanismo il tutto risulta molto banale.

Che fare se volessimo creare un decoratore parametrizzabile, cioè al quale vogliamo passare degli argomenti (come se fosse una funzione) nel momento in cui viene usato?

L'esempio, riprendendo il discorso di prima, è il seguente. Vogliamo un decoratore a cui specificare se ritornare il json in formato più leggibile (con le indentazioni e gli a capo per interderci) oppure normalmente senza occuparci troppo del formato visivo.

Per farlo, ecco il codice che propongo:

def as_json(indent=False):
    def as_json_dec(func):
        def func_wrapper(*args, **kargs):
            d = func(*args, **kargs)
            if indent:
                return json.dumps(d, indent=4)
            else:
                return json.dumps(d)
        return func_wrapper
    return as_json_dec

In questo caso, as_json non è più una decorator, ma una funzione che ritorna a sua volta un decoratore (as_json_dec). Il parametro passato alla fuzione as_json viene utilizzato all'interno del decoratore per modificarne il suo comportamento.

Questo decoratore può essere utilizzato come segue:

@as_json()
def dict_name_age(name, age):
    return dict(name=name, age=age)

o, in alternativa, in questo modo:

@as_json(True)
def dict_name_age(name, age):
    return dict(name=name, age=age)

Nel secondo caso, il risultato è il seguente:

{
    "name": "ludovico",
    "age": 12
}

Esempio Reale: decoratore in Flask

Uno dei miei framework preferiti è Flask, un microframework in python per creare siti web (lato server). Flask è molto potente per la creazione di API REST. Per chi non lo sapesse, le API REST sono una soluzione che permette ad un'applicazione generica (web, mobile, desktop) di scambiare dati con un server, indipendentemente dal linguaggio di programmazione scelto e dalla piattaforma.

Entrerò nel dettaglio sulle funzionalità di REST in futuro, vediamo però ora come utilizzare il nostro decoratore in Flask per ritornare dati in JSON dalla nostra applicazione.

WebApp Flask

Vogliamo implementare una semplice app Flask che ritorna una dato json, invece di una pagina in HTML. Per farlo, esiste un'estensione di Flask che già implementa questa funzionalià, ma voglio farvi vedere come, utilizzando i decoratori, la cosa è molto semplice da implementare.

Prima di tutto, notiamo che Flask mette a disposizione la funzione jsonify() che ha un comportamento simile a json.dumps(), ma ritorna una risposta HTML completa, settando anche nell'header il tipo di contenuto come application/json (in questo modo i browser capiranno che stiamo utilizzando dei dati di tipo json).

Per farlo, quindi, definitiamo il nostro decoratore come segue:

from flask import Flask, jsonify

def as_json(func):
    def func_wrapper(*args, **kargs):
        d = func(*args, **kargs)
        return jsonify(d)
    return func_wrapper

E utilizziamolo all'interno della nostra route flask, nel seguente modo:

app = Flask(__name__)

@app.route('/')
@as_json
def index():
    res = {'risposta': 'Ciao a Tutti!'}
    return res

if __name__ == '__main__':
    app.run(debug=True)

Se a questo punto lanciamo la nostra applicazione Flask e accediamo al browser, vedremo che la riposta viene correttamente interpretata come un JSON.

Risposta JSON

Ovviamente questo decoratore ha ancora molti margini di miglioramento: ad esempio, non possiamo specificare lo status code di risposta nella nostra funzione decorata.

Conclusioni

L'esempio qui presentato è molto banale, ma fa capire le potenzialità dei decoratori! Fatemi sapere nei commenti come implementate voi questo tipo di operazioni.

Ti è piaciuto questo post?

Registrati alla newsletter per rimanere sempre aggiornato!

Ci tengo alla tua privacy. Leggi di più sulla mia Privacy Policy.

Ti potrebbe anche interessare

HB Cloud Tutorial #1 - Uso dei Led
Iniziamo ad utilizzare la piattaforma di Cloud Robotics
HB Cloud Tutorial #2 - Uso dei Bottoni
Rieccomi con il secondo tutorial legato all'uso dei bottoni per il robot **DotBot-ROS**. In questo tutorial, vedremo come configurare ed utilizzare in Python un bottone attaccato ad un pin GPIO del Raspberry Pi 3.
HB Cloud Tutorial #3 - I Motori
I Motori sono una delle parti essenziali dei robot. In questo tutorial, vederemo come è possibile in modo semplice ed intuitivo implementare un programma in Python che controlla i motori in base a comandi inviati via Wifi al Robot.
Inviare Goals alla Navigation Stack - versione nodo ROS Python
Inviare un goal all ROS navigation stack utilizzando un nodo Python
Controllare siBOT dalla piattaforma HBR
Come controllare il manipolatore siBOT utilizzando la piattaforma HBR
Sviluppare un rilevatore di fiamma con la visione artificiale
Sviluppare un rilevatore di fiamma con la visione artificiale
Scriviamo un Blog in Python e Flask
Tutorial su come implementare, a partire da zero, un blog personale utilizzando Python e Flask! Prima parte!
Un laboratorio di Fisica con Arduino e Python
Primi esperimenti con Arduino e Python per realizzare un semplice laboratorio di fisica sfruttando la potenza di Python e la versatilità di Arduino
Canopy: una Pythonica alternativa a Matlab
Presento questo interessante tool python che può essere considerato una buona alternativa a Matlab per l'analisi dei dati!
Spyder, un'altra alternativa in Python a Matlab
Una velocissima prova del tool interattivo Spyder per l'analisi scientifica in Python
Simuliamo il moto parabolico in Python e Spyder
Un piccolo tutorial per iniziare ad utilizzare Spyder con Python
Python + Arduino = Nanpy
Programmare Arduino in Python con Nanpy
Utilizzo di Nanpy con il sensore di temperatura/umidità della famiglia DHT
Come utilizzare Nanpy col sensore DHT di temperatura e Umidità
Accendere led con Arduino e Telegram
Un bot telegram in grado di controllare Arduino realizzato da 3 ragazzi del Liceo Stampacchia
Implementiamo un bot Telegram con Python
Una semplice guida per iniziare a muovere i primi passi nello sviluppo di chatbot Telegram con Python
Pillole di Python: pyscreenshot
Una semplice tutorial che mostra il funzionamento della libreria pyscreenshot
TDD con Flask e PyTest per lo sviluppo di API REST. Parte 1
Tutorial su come usare il Test Driver Development (TDD) con Flask e PyTest per sviluppare delle semplici API REST
Implementiamo un bot Telegram con Python - I Comandi
Vediamo come gestire i comandi del nostro bot in Telegram
4 (+1) Libri su Python (in Inglese) da cui imparare
Una lista di libri su Python (in Inglese) da cui ho imparato a programmare
Virtualenv: gestiamo meglio le dipendenze in Python
A cosa servono e come si utilizzano i virtualenv Python
Leggere i codici a barre con OpenCV e zbar in Python
Come usare Python per leggere i codici a barre degli alimenti e ricavarne alcune informazioni utili
TDD con Flask e PyTest per lo sviluppo di API REST. Parte 2
Tutorial su come usare il Test Driver Development (TDD) con Flask e PyTest per sviluppare delle semplici API REST
Sviluppiamo un bot Telegram che legge i codici a barre degli alimenti
Implementiamo un bot Telegram in grado di leggere ed analizzare le immagini per la lettura ed interpretazione dei codici a barre
TDD con Flask e PyTest per lo sviluppo di API REST. Parte 3
Tutorial su come usare il Test Driver Development (TDD) con Flask e PyTest per sviluppare delle semplici API REST
Divertiamoci sviluppando UI da terminale con ASCIIMATICS
Le UI da terminale fanno molto anni '80, però sono sempre diventerti da implementare. Oggi vi voglio introdurre ad una semplice libreria per creare questo tipo di applicazione.
Sviluppiamo un Robot con tecnologie Open Source
Inizio una serie di videoguide, in cui voglio introdurvi al mondo della robotica di servizio in modo pratico, facendo vedere come è possibilile, sfruttando tecnologie completamente Open Source, quali Arduino, Raspberry Pi, ROS e Docker, costruire un piccolo robot di Servizio.
Parliamo come GMaps: come creare file audio con gtts (Google Text to Speech) in Python
gtts è una libreria in Python per sfruttare le API di Google Text to Speech per generare file audio dal testo
Robot Open Source - Introduzione a Docker
È disponibile il video "Introduzione a Docker".
I chatbot possono Parlare? Sviluppiamo un bot telegram che manda messaggi vocali
Usiamo le API di sintesi vocale di google per creare un bot in grado di mandare messaggi vocali
Robot Open Source - Docker e Raspberry
È disponibile il video "Docker e Raspberry".