Scrivi i tuoi decoratori Python

Panoramica

Nell'articolo Deep Dive Into Python Decorators, ho introdotto il concetto dei decoratori Python, dimostrato molti decoratori interessanti e spiegato come usarli.

In questo tutorial ti mostrerò come scrivere i tuoi decoratori. Come vedrai, scrivere i tuoi decoratori ti dà un grande controllo e abilita molte funzionalità. Senza decoratori, quelle capacità richiederebbero un numero elevato di bozze suscettibili di errori e ripetute che ingombrano il codice o meccanismi completamente esterni come la generazione di codice.

Un breve riassunto se non sai nulla dei decoratori. Un decoratore è un callable (funzione, metodo, classe o oggetto con a) chiamata()) che accetta un callable come input e restituisce un callable come output. In genere, il richiamabile richiamato fa qualcosa prima e / o dopo aver chiamato l'input callable. Applichi il decoratore usando @ sintassi. Un sacco di esempi in arrivo ...

Hello World Decorator

Iniziamo con un "Ciao mondo!" decoratore. Questo decoratore sostituirà totalmente qualsiasi callable decorato con una funzione che stampa solo "Hello World!".

python def hello_world (f): def decorato (* args, ** kwargs): stampa 'Hello World!' ritorno decorato

Questo è tutto. Vediamolo in azione e poi spieghiamo i diversi pezzi e come funziona. Supponiamo di avere la seguente funzione che accetta due numeri e stampa il loro prodotto:

python def moltiplicare (x, y): print x * y

Se invochi, ottieni ciò che ti aspetti:

moltiplicare (6, 7) 42

Decoriamo con il nostro Ciao mondo decoratore annotando il moltiplicare funzione con @Ciao mondo.

python @hello_world def moltiplicare (x, y): print x * y

Ora, quando chiami moltiplicare con qualsiasi argomento (compresi i tipi di dati errati o il numero errato di argomenti), il risultato è sempre "Hello World!" stampato.

"Python moltiplicare (6, 7) Hello World!

moltiplicare () Ciao mondo!

moltiplicare ('zzz') Hello World! "

OK. Come funziona? La funzione di moltiplicazione originale è stata completamente sostituita dalla funzione decorata nidificata all'interno del Ciao mondo decoratore. Se analizziamo la struttura del Ciao mondo decoratore allora vedrai che accetta l'input callable f (che non è usato in questo semplice decoratore), definisce una funzione annidata chiamata decorato accetta qualsiasi combinazione di argomenti e argomenti di parole chiave (decorato def (* args, ** kwargs)), e infine restituisce il decorato funzione.

Funzione di scrittura e decoratori di metodi

Non c'è differenza tra scrivere una funzione e un decoratore di metodi. La definizione del decoratore sarà la stessa. L'input callable sarà una funzione normale o un metodo associato.

Verifichiamo questo. Ecco un decoratore che stampa l'input callable e digita prima di invocarlo. Questo è molto tipico per un decoratore per eseguire qualche azione e continuare invocando l'originale callable.

python def print_callable (f): def decorato (* args, ** kwargs): print f, type (f) return f (* args, ** kwargs) reso decorato

Notare l'ultima riga che richiama l'input callable in un modo generico e restituisce il risultato. Questo decoratore non è intrusivo, nel senso che puoi decorare qualsiasi funzione o metodo in un'applicazione funzionante, e l'applicazione continuerà a funzionare perché la funzione decorata richiama l'originale e ha solo un piccolo effetto collaterale prima.

Vediamolo in azione. Decorerò sia la nostra funzione di moltiplicazione che un metodo.

"python @print_callable def moltiplicare (x, y): print x * y

classe A (oggetto): @print_callable def foo (self): stampa 'foo () qui "

Quando chiamiamo la funzione e il metodo, il callable viene stampato e quindi eseguono il loro compito originale:

"moltiplicare python (6, 7) 42

A (). Foo () foo () qui "

Decoratori con argomenti

I decoratori possono anche prendere argomenti. Questa capacità di configurare il funzionamento di un decoratore è molto potente e consente di utilizzare lo stesso decoratore in molti contesti.

Supponiamo che il tuo codice sia troppo veloce e il tuo capo ti chiede di rallentarlo un po 'perché stai facendo sembrare cattivi gli altri membri del team. Scriviamo un decoratore che misura il tempo di esecuzione di una funzione e se viene eseguito in meno di un certo numero di secondi t, aspetterà fino alla scadenza di t secondi e quindi ritorna.

Ciò che è diverso ora è che il decoratore stesso prende una discussione t che determina il runtime minimo e le diverse funzioni possono essere decorate con diversi runtime minimi. Inoltre, si noterà che quando si introducono gli argomenti del decoratore, sono richiesti due livelli di nidificazione:

"tempo di importazione python

def minimum_runtime (t): def decorated (f): def wrapper (args, ** kwargs): start = time.time () result = f (args, ** kwargs) runtime = time.time () - avvia se runtime < t: time.sleep(t - runtime) return result return wrapper return decorated"

Disimballiamo. Lo stesso decoratore: la funzione minimum_runtime prende una discussione t, che rappresenta il runtime minimo per il callable decorato. L'input callable f è stato "spinto" fino al nidificato decorato funzione, e gli argomenti callable di input sono stati "spinti" in un'altra funzione nidificata involucro.

La logica attuale ha luogo all'interno del involucro funzione. L'ora di inizio è registrata, l'originale callable f viene invocato con i suoi argomenti e il risultato viene archiviato. Quindi il runtime viene controllato e se è inferiore al minimo t poi dorme per il resto del tempo e poi ritorna.

Per testarlo, creerò un paio di funzioni che chiamano moltiplicare e decorarle con ritardi diversi.

"python @minimum_runtime (1) def slow_multiply (x, y): moltiplica (x, y)

@minimum_runtime (3) def slower_multiply (x, y): moltiplica (x, y) "

Ora, chiamerò moltiplicare direttamente così come le funzioni più lente e misurare il tempo.

"tempo di importazione python

funcs = [moltiplicare, slow_multiply, più lento_multiply] per f in func: start = time.time () f (6, 7) print f, time.time () - start "

Ecco l'output:

pianura 42 1.59740447998e-05 42 1.00477004051 42 3,00489807129

Come puoi vedere, il multiplo originale non ha impiegato quasi tempo e le versioni più lente sono state effettivamente ritardate in base al tempo di esecuzione minimo previsto.

Un altro fatto interessante è che la funzione decorata eseguita è l'involucro, che ha senso se si segue la definizione del decorato. Ma quello potrebbe essere un problema, specialmente se abbiamo a che fare con i decoratori dello stack. Il motivo è che molti decoratori controllano anche il loro input richiamabile e ne controllano il nome, la firma e gli argomenti. Le sezioni seguenti esamineranno questo problema e forniranno consigli per le migliori pratiche.

Decoratori di oggetti

Puoi anche usare oggetti come decoratori o oggetti di ritorno dai tuoi decoratori. L'unico requisito è che abbiano un __chiamata__() metodo, quindi sono chiamabili Ecco un esempio per un decoratore basato su oggetti che conta quante volte viene chiamata la sua funzione target:

classe python Counter (oggetto): def __init __ (self, f): self.f = f self.called = 0 def __call __ (self, * args, ** kwargs): self.called + = 1 return self.f (* args, ** kwargs)

Eccolo in azione:

"python @Counter def bbb (): stampa 'bbb'

bbb () bbb

bbb () bbb

bbb () bbb

stampa bbb.called 3 "

Scegliere tra decoratori basati su funzioni e oggetti

Questa è principalmente una questione di preferenza personale. Le funzioni annidate e le chiusure di funzioni forniscono tutta la gestione dello stato offerta dagli oggetti. Alcune persone si sentono più a loro agio con classi e oggetti.

Nella prossima sezione, discuterò di decoratori ben educati, e i decoratori a base di oggetti fanno un piccolo lavoro in più per essere ben educati.

Decoratori ben fatti

Spesso i decoratori di scopo generale possono essere impilati. Per esempio:

python @ decorator_1 @ decorator_2 def foo (): stampa 'foo () qui'

Quando impilano i decoratori, il decoratore esterno (decoratore_1 in questo caso) riceverà il callable restituito dal decoratore interno (decoratore_2). Se decorator_1 dipende in qualche modo dal nome, argomenti o docstring della funzione originale e decorator_2 sono implementati in modo ingenuo, allora decorator_2 vedrà non vedere le informazioni corrette dalla funzione originale, ma solo il callable restituito da decorator_2.

Ad esempio, ecco un decoratore che verifica che il nome della sua funzione di destinazione sia tutto in minuscolo:

python def check_lowercase (f): def decorato (* args, ** kwargs): assert f.func_name == f.func_name.lower () f (* args, ** kwargs) reso decorato

Decoriamo una funzione con esso:

python @check_lowercase def Foo (): stampa 'Foo () qui'

Calling Foo () genera un'asserzione:

"plain In [51]: Foo () - AssertionError Traceback (ultima chiamata più recente)

nel () ----> 1 Foo () in decorato (* args, ** kwargs) 1 def check_lowercase (f): 2 def decorato (* args, ** kwargs): ----> 3 assert f.func_name == f.func_name.lower () 4 return decorato "Ma se impiliamo il decoratore ** check_lowercase ** su un decoratore come ** hello_world ** che restituisce una funzione nidificata chiamata 'decorata' il risultato è molto diverso:" python @check_lowercase @hello_world def Foo (): print ' Foo () qui 'Foo () Hello World! "Il decoratore ** check_lowercase ** non ha sollevato un'affermazione perché non ha visto il nome della funzione' Foo '. Questo è un problema serio. Il comportamento corretto per un decoratore è quello di preservare il più possibile gli attributi della funzione originale. Vediamo come è fatto. Creerò ora un decoratore di shell che chiama semplicemente il suo input callable, ma conserva tutte le informazioni dalla funzione di input: il nome della funzione, tutti i suoi attributi (nel caso in cui un decoratore interno aggiungesse alcuni attributi personalizzati) e la sua docstring. "python def passthrough (f): def decorato (* args, ** kwargs): f (* args , ** kwargs) decorato .__ nome__ = f .__ nome__ decorato .__ nome__ = f .__ modulo__ decorato .__ dict__ = f .__ dict__ decorato .__ doc__ = f .__ doc__ reso decorato "Ora, decoratori impilati in cima al ** passthrough ** decoratore funzionerà proprio come se decorassero direttamente la funzione target. "python @check_lowercase @passthrough def Foo (): stampa 'Foo () qui" ### Usando @wraps Decorator Questa funzionalità è così utile che la libreria standard ha uno speciale decoratore nel modulo functools chiamato ['wraps'] (https://docs.python.org/2/library/functools.html#functools.wraps) per aiutare a scrivere decoratori adeguati che funzionino bene con altri decoratori. Decora semplicemente all'interno del tuo decoratore la funzione restituita con ** @ wraps (f) **. Vedi quanto più conciso ** passthrough ** appare quando si utilizza ** wraps **: "python da functools import wraps def passthrough (f): @wraps (f) def decorato (* args, ** kwargs): f (* args, ** kwargs) resi decorati "Consiglio vivamente di usarlo sempre a meno che il decoratore non sia progettato per modificare alcuni di questi attributi. ## Scrittura di decoratori di classe I decoratori di classe sono stati introdotti in Python 3.0. Operano su un'intera classe. Un decoratore di classe viene richiamato quando viene definita una classe e prima che vengano create tutte le istanze. Ciò consente al decoratore di classe di modificare praticamente ogni aspetto della classe. In genere aggiungerai o decorerai più metodi. Passiamo a un esempio di fantasia: supponiamo di avere una classe chiamata "AwesomeClass" con una serie di metodi pubblici (metodi il cui nome non inizia con un trattino basso come __init__) e hai una classe di test basata su unittests chiamata "AwesomeClassTest" '. AwesomeClass non è semplicemente fantastico, ma anche molto critico e vuoi assicurarti che se qualcuno aggiunge un nuovo metodo a AwesomeClass, aggiunge anche un metodo di prova corrispondente a AwesomeClassTest. Ecco AwesomeClass: "classe python AwesomeClass: def awesome_1 (self): return 'awesome!' def awesome_2 (self): return 'awesome! awesome! "Ecco AwesomeClassTest:" python dall'unittest import TestCase, classe principale AwesomeClassTest (TestCase): def test_awesome_1 (self): r = AwesomeClass (). awesome_1 () self.assertEqual ('awesome!', r) def test_awesome_2 (self): r = AwesomeClass (). awesome_2 () self.assertEqual ('awesome! awesome!', r) if __name__ == '__main__': main () "Ora, se qualcuno aggiunge un metodo ** awesome_3 ** con un bug, i test passeranno comunque perché non esiste un test che chiama ** awesome_3 **. Come puoi assicurarti che ci sia sempre un metodo di prova per ogni metodo pubblico? Bene, tu scrivi un decoratore di classe, naturalmente. Il decoratore della classe @ensure_tests decorerà AwesomeClassTest e si assicurerà che ogni metodo pubblico abbia un metodo di test corrispondente. "Python def verify_tests (cls, target_class): test_methods = [m per m in cl .__ dict__ se m.startswith ('test_' )] public_methods = [k per k, v in target_class .__ dict __. items () se callable (v) e non k.startswith ('_')] # Strip 'test_' prefisso dai nomi dei metodi di prova test_methods = [m [5 :] per m in test_methods] if set (test_methods)! = set (public_methods): raise RuntimeError ('Test / public methods mismatch!') return cls "Sembra abbastanza buono, ma c'è un problema. I decoratori di classe accettano solo un argomento: la classe decorata. Il decor_tests ha bisogno di due argomenti: la classe e la classe di destinazione. Non sono riuscito a trovare un modo per avere decoratori di classe con argomenti simili ai decoratori di funzioni. Non avere paura. Python ha la funzione [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) solo per questi casi. "Python @partial (sure_tests, target_class = AwesomeClass) class AwesomeClassTest (TestCase): def test_awesome_1 (self): r = AwesomeClass (). Awesome_1 () self.assertEqual ('awesome!', R) def test_awesome_2 (self): r = AwesomeClass (). Awesome_2 () self.assertEqual (' awesome! awesome! ', r) if __name__ ==' __main__ ': main () "L'esecuzione dei test ha esito positivo perché tutti i metodi pubblici, ** awesome_1 ** e ** awesome_2 **, hanno metodi di test corrispondenti, * * test_awesome_1 ** e ** test_awesome_2 **. "-------------------------------------- -------------------------------- Esegui 2 test in 0.000 OK "Aggiungiamo un nuovo metodo ** awesome_3 ** senza un test corrispondente ed esegui di nuovo i test. "python class AwesomeClass: def awesome_1 (self): return 'awesome!' def awesome_2 (self): return 'awesome! awesome!' def awesome_3 (self): return 'awesome! awesome! awesome! "L'esecuzione dei test genera nuovamente il seguente output:" python3 a.py Traceback (ultima chiamata più recente): File "a.py", riga 25, in .