Deep Dive in Python Decorators

Panoramica

I decoratori Python sono una delle mie caratteristiche preferite di Python. Sono l'implementazione più facile da usare * e * per gli sviluppatori di programmi orientati agli aspetti che ho visto in qualsiasi linguaggio di programmazione. 

Un decoratore consente di aumentare, modificare o sostituire completamente la logica di una funzione o metodo. Questa descrizione asciutta non rende giustizia ai decoratori. Una volta che inizi a utilizzarli, scoprirai un intero universo di applicazioni pulite che ti aiutano a mantenere il codice stretto e pulito e a spostare importanti attività "amministrative" dal flusso principale del tuo codice e in un decoratore. 

Prima di entrare in alcuni esempi interessanti, se si desidera esplorare un po 'di più l'origine dei decoratori, i decoratori di funzioni sono apparsi per primi in Python 2.4. Vedi PEP-0318 per un'interessante discussione sulla storia, la logica e la scelta del nome "decoratore". I decoratori di classe sono apparsi per primi in Python 3.0. Vedi PEP-3129, che è piuttosto breve e si basa su tutti i concetti e le idee dei decoratori di funzioni.

Esempi di decoratori freddi

Ci sono così tanti esempi che mi viene difficile scegliere. Il mio obiettivo qui è aprire la tua mente alle possibilità e presentarti funzionalità super-utili che puoi aggiungere immediatamente al tuo codice annotando letteralmente le tue funzioni con un solo liner.

Gli esempi classici sono i decoratori incorporati @staticmethod e @classmethod. Questi decoratori trasformano un metodo di classe corrispondentemente in un metodo statico (non viene fornito un argomento self first) o un metodo di classe (il primo argomento è la classe e non l'istanza).

I decoratori classici

classe A (oggetto): @classmethod def foo (cls): print cls .__ name__ @staticmethod def bar (): print 'Non ho alcun uso per l'istanza o la classe' A.foo () A.bar () 

Produzione:

A Non ho alcuna utilità per l'istanza o la classe

I metodi statici e di classe sono utili quando non hai un'istanza in mano. Sono usati molto, ed è stato davvero ingombrante applicarli senza la sintassi del decoratore.

Memoizzazione

@Memoize decorator memorizza il risultato della prima chiamata di una funzione per un particolare set di parametri e lo memorizza nella cache. Le successive chiamate con gli stessi parametri restituiscono il risultato memorizzato nella cache. 

Questo potrebbe essere un enorme potenziamento delle prestazioni per le funzioni che richiedono un'elaborazione costosa (ad esempio raggiungere un database remoto o chiamare più API REST) ​​e vengono chiamate spesso con gli stessi parametri.

@memoize def fetch_data (items): "" "Fai del lavoro serio qui" "" result = [fetch_item_data (i) per i in items] return result 

Programmazione basata su contratto

Che ne dici di un paio di decoratori chiamati @precondition e @postcondition per convalidare l'argomento di input e il risultato? Considera la seguente semplice funzione:

def add_small ints (a, b): "" "Aggiungi due interi la cui somma è ancora un" "" restituisce un + b

Se qualcuno lo chiama con interi grandi o long o anche stringhe, riuscirà tranquillamente, ma violerà il contratto che il risultato deve essere un int. Se qualcuno lo chiama con tipi di dati non corrispondenti, otterrai un errore di runtime generico. È possibile aggiungere il seguente codice alla funzione:

def add_small ints (a, b): "" "Aggiungi due ints nella cui somma è ancora un int" "" assert (isinstance (a, int), 'a deve essere un int') assert (isinstance (a, int ), 'b deve essere un int') result = a + b assert (isinstance (result, int), 'gli argomenti sono troppo grandi. sum non è un int') risultato di ritorno 

La nostra bella linea add_small_ints () la funzione è appena diventata un brutto pantano con affermazioni brutte. In una funzione del mondo reale può essere davvero difficile vedere a colpo d'occhio cosa sta effettivamente facendo. Con i decoratori, le condizioni pre e post possono uscire dal corpo della funzione:

@precondition (isinstance (a, int), 'a deve essere un int') @precondition (isinstance (b, int), 'b deve essere un int') @postcondition (isinstance (retval, int), 'gli argomenti sono troppo grande. sum non è un int ') def add_small ints (a, b): "" "Aggiungi due ints nella cui somma è ancora un int" "" return a + b 

Autorizzazione

Supponiamo che tu abbia una classe che richiede l'autorizzazione tramite un segreto per tutti i suoi numerosi metodi. Essendo lo sviluppatore Python consumato, dovresti optare per un decoratore di metodi @authorized come in:

class SuperSecret (oggetto): @authorized def f_1 (* args, secret): "" "" "" @authorized def f_2 (* args, secret): "" "" "" ... @authorized def f_100 (* args, secret ): "" "" ""

Questo è sicuramente un buon approccio, ma è un po 'fastidioso farlo ripetutamente, specialmente se hai molte di queste classi. 

Più in particolare, se qualcuno aggiunge un nuovo metodo e si dimentica di aggiungere la decorazione @authorized, hai un problema di sicurezza a portata di mano. Non avere paura. I decoratori di classe Python 3 ti hanno dato le spalle. La seguente sintassi ti consentirà (con la corretta definizione del decoratore di classe) di autorizzare automaticamente ogni metodo delle classi target:


classe @authorized SuperSecret (oggetto): def f_1 (* args, secret): "" "" "" def f_2 (* args, secret): "" "" "" ... def f_100 (* args, secret): "" "" "" "

Tutto ciò che devi fare è decorare la classe stessa. Si noti che il decoratore può essere intelligente e ignorare un metodo speciale come __dentro__() oppure può essere configurato per essere applicato a un particolare sottoinsieme, se necessario. Il cielo (o la tua immaginazione) è il limite.

Altri esempi

Se vuoi seguire ulteriori esempi, controlla PythonDecoratorLibrary. 

Cos'è un decoratore?

Ora che hai visto alcuni esempi in azione, è tempo di svelare la magia. La definizione formale è che un decoratore è un callable che accetta un callable (il target) e restituisce un callable (decorato) che accetta gli stessi argomenti del target originale. 

Woah! sono un sacco di parole accatastate l'una sull'altra in modo incomprensibile. Innanzitutto, cos'è un callable? Un callable è solo un oggetto Python che ha a __chiamata__() metodo. Questi sono in genere funzioni, metodi e classi, ma è possibile implementare a __chiamata__() metodo su una delle tue classi e quindi le istanze di classe diventeranno anche callables. Per verificare se un oggetto Python è callable, puoi usare la funzione built-in callable ():


callable (len) True callable ('123') Falso

Si noti che il callable () la funzione è stata rimossa da Python 3.0 e riportata in Python 3.2, quindi se per qualche motivo si utilizza Python 3.0 o 3.1, è necessario verificare l'esistenza di __chiamata__ attributo come in hasattr (len, '__call__').

Quando si prende un decoratore di questo tipo e lo si applica con la sintassi @ a qualche chiamabile, il callable originale viene sostituito con il callable restituito dal decoratore. Questo può essere un po 'difficile da comprendere, quindi illustriamolo esaminando le viscere di alcuni semplici decoratori.

Decoratori di funzioni

Un decoratore di funzioni è un decoratore che viene utilizzato per decorare una funzione o un metodo. Supponiamo di voler stampare la stringa "Sì, funziona!" ogni volta viene chiamata una funzione o un metodo decorato prima di richiamare effettivamente la funzione originale. Ecco un modo non decoratore per raggiungerlo. Ecco la funzione foo () che stampa "foo () qui":

def foo (): print 'foo () here' foo () Output: foo () qui 

Ecco il modo brutto per ottenere il risultato desiderato:

original_foo = foo def decorated_foo (): print 'Sì, funziona!' original_foo () foo = decorated_foo foo () Output: Sì, funziona! foo () qui

Ci sono diversi problemi con questo approccio:

  • È molto lavoro.
  • Si inquina lo spazio dei nomi con nomi intermedi come original_foo () e decorated_foo ().
  • Devi ripeterlo per ogni altra funzione che desideri decorare con la stessa capacità.

Un decoratore che realizza lo stesso risultato ed è anche riutilizzabile e componibile assomiglia a questo:

def yeah_it_works (f): def decorato (* args, ** kwargs): print 'Sì, funziona' return f (* args, ** kwargs) reso decorato

Si noti che yeah_it_works () è una funzione (quindi chiamabile) che accetta un callable ** f ** come argomento e restituisce un callable (la funzione nidificata ** decorata **) che accetta qualsiasi numero e tipo di argomenti.

Ora possiamo applicarlo a qualsiasi funzione:


@yeah_it_works def f1 () stampa 'f1 () qui' @yeah_it_works def f2 () stampa 'f3 () qui' @yeah_it_works def f3 () stampa 'f3 () qui' f1 () f2 () f3 () Output: Sì, funziona f1 () qui Sì, funziona f2 () qui Sì, funziona f3 () qui

Come funziona? L'originale f1, f2 e f3 le funzioni sono state sostituite dalla funzione nidificata decorata restituita da yeah_it_works. Per ogni singola funzione, il catturato f callable è la funzione originale ( f1f2 o f3), quindi la funzione decorata è diversa e fa la cosa giusta, che è la stampa "Sì, funziona!" e quindi invoca la funzione originale f.

Decoratori di classe

I decoratori di classe operano a un livello superiore e decorano un'intera classe. Il loro effetto ha luogo nel tempo di definizione della classe. Puoi usarli per aggiungere o rimuovere metodi di qualsiasi classe decorata o anche per applicare i decoratori di funzioni a un intero insieme di metodi. 

Supponiamo di voler tenere traccia di tutte le eccezioni generate da una particolare classe in un attributo di classe. Supponiamo di avere già un decoratore di funzioni chiamato track_exceptions_decorator che esegue questa funzionalità. Senza un decoratore di classe, puoi applicarlo manualmente ad ogni metodo o ricorrere a metaclassi. Per esempio:


classe A (oggetto): @track_exceptions_decorator def f1 (): ... @track_exceptions_decorator def f2 (): ... @track_exceptions_decorator def f100 (): ... 

Un decoratore di classe che raggiunge lo stesso risultato è:


def track_exception (cls): # Ottieni tutti gli attributi callable della classe callable_attributes = k: v per k, v in cls .__ dict __. items () se callable (v) # Decora ogni attributo callable della classe di input per nome , func in callable_attributes.items (): decorated = track_exceptions_decorator (func) setattr (cls, nome, decorato) return cls @track_exceptions classe A: def f1 (self): print ('1') def f2 (self): print ( '2')

Conclusione

Python è ben noto per la sua flessibilità. I decoratori lo portano al livello successivo. È possibile inserire preoccupazioni trasversali nei decoratori riutilizzabili e applicarle a funzioni, metodi e intere classi. Consiglio vivamente che ogni serio sviluppatore Python familiarizzi con i decoratori e tragga pieno vantaggio dai loro benefici.