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 @
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.
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)
A (). Foo ()
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
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.
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 "
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.
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)