Scrieți propriile dvs. decoratori Python

Prezentare generală

În articolul Deep Dive Into Python Decorators, am introdus conceptul de decoratori Python, am demonstrat mulți decoratori răcoritori și am explicat cum să le folosesc.

În acest tutorial vă voi arăta cum să vă scrieți propriile decoratoare. După cum veți vedea, scrierea propriilor decoratori vă oferă mult control și permite multe posibilități. Fără decoratori, aceste capacități ar necesita o mulțime de eroare-predispuse și boilerplate repetitive, care aglomera codul dvs. sau complet mecanisme externe cum ar fi generarea de cod.

O recapitulare rapidă dacă nu știți nimic despre decoratori. Un decorator este un callabil (funcție, metodă, clasă sau obiect cu a apel()) care acceptă o intrare callabilă și returnează o ieșire callabilă ca ieșire. În mod obișnuit, apelantul returnat face ceva înainte și / sau după ce a sunat la apelul de intrare. Aplicați decoratorul utilizând funcția @ sintaxă. O mulțime de exemple în curând ...

Decoratorul Hello World

Să începem cu o lume "Hello world!" decorator. Acest decorator va înlocui în totalitate orice decorat cu o funcțiune care imprimă "Hello World!".

python def hello_world (f): Def decorat (* args, ** kwargs): tipăriți 'Hello World!' retur decorat

Asta e. Să o vedem în acțiune și apoi să explicăm piesele diferite și cum funcționează. Să presupunem că avem următoarea funcție care acceptă două numere și imprimă produsul lor:

(x, y): imprima x * y

Dacă invocați, obțineți ceea ce vă așteptați:

înmulțiți (6, 7) 42

Să o decorăm cu noi Salut Lume decorator prin adnotarea multiplica funcția cu @Salut Lume.

(x, y): print x * y

Acum, când suni multiplica cu argumente (inclusiv tipuri greșite de date sau număr greșit de argumente), rezultatul este întotdeauna "Hello World!" imprimate.

"python multiplica (6, 7) Hello World!

multiplica () Hello World!

multiplicați ("zzz") Hello World! "

O.K. Cum functioneazã? Funcția de multiplicare originală a fost complet înlocuită de funcția decorată în interior Salut Lume decorator. Dacă analizăm structura Salut Lume decorator, atunci veți vedea că acceptă intrarea apelată f (care nu este folosit în acest decorator simplu), definește o funcție imbricată numită decorate care acceptă orice combinație de argumente și argumente de cuvinte cheie (def decorat (* args, ** kwargs)), și în cele din urmă returnează decorate funcţie.

Scrierea funcției și a metodelor de decorare

Nu există nicio diferență între scrierea unei funcții și a unui decorator de metode. Definitia decoratorului va fi aceeasi. Avansul de intrare va fi fie o funcție obișnuită, fie o metodă legată.

Să verificăm asta. Aici este un decorator care imprimă doar intrarea callabilă și tastați înainte de ao invoca. Acest lucru este foarte tipic pentru ca un decorator să efectueze o anumită acțiune și să continue să invocă apelul inițial.

python def print_callable (f): def decorat (* args, ** kwargs): print f, tip (f) return f (* args, ** kwargs)

Rețineți ultima linie care invocă intrarea apelată într-un mod generic și returnează rezultatul. Acest decorator nu este intruziv în sensul că puteți decora orice funcție sau metodă într-o aplicație de lucru și aplicația va continua să funcționeze deoarece funcția decorată invocă originalul și are doar un efect secundar înainte.

Să o vedem în acțiune. Voi decora atât funcția noastră multiplă cât și o metodă.

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

clasa A (obiect): @print_callable def foo (self): print 'foo () aici "

Atunci când apelăm funcția și metoda, callabilul este imprimat și apoi își îndeplinesc sarcina inițială:

"multiplicarea python (6, 7) 42

A (). Foo () foo () aici "

Decoratorii cu argumente

Decoratorii pot lua și argumente. Această abilitate de a configura funcționarea unui decorator este foarte puternică și vă permite să folosiți același decorator în multe contexte.

Să presupunem că codul dvs. este prea rapid și șeful dvs. vă cere să o încetiniți puțin pentru că faceți alți membri ai echipei să pară rău. Să scriem un decorator care măsoară cât timp funcționează o funcție și dacă rulează în mai puțin de un anumit număr de secunde T, acesta va aștepta până t expiră și apoi se va întoarce.

Ce este diferit acum este faptul că decoratorul însuși ia un argument T care determină durata minimă de rulare, iar funcțiile diferite pot fi decorate cu durate minime diferite. De asemenea, veți observa că atunci când introduceți argumente pentru decorator, sunt necesare două nivele de cuibărit:

"timpul de import pentru Python

def minimum_runtime (t): def decorat (f): def wrap (args, ** kwargs): start = time.time () rezultat = f (args, ** kwargs) runtime = time.time () - porni dacă este timpul de execuție < t: time.sleep(t - runtime) return result return wrapper return decorated"

Să-l despachetăm. Decoratorul însuși - funcția minimum_runtime ia un argument T, care reprezintă durata minimă de rulare pentru cei care au fost apelați. Intrarea este apelabilă f a fost "împins în jos" la imbricate decorate funcția și argumentele care pot fi apelate sunt "împinse" până la o altă funcție imbricată învelitoare.

Logica reală are loc în interiorul învelitoare funcţie. Timpul de pornire este înregistrat, originalul poate fi sunat f este invocat cu argumentele sale, iar rezultatul este stocat. Apoi, timpul de execuție este verificat și dacă este mai mic decât minimul T apoi doarme pentru restul timpului și apoi se întoarce.

Pentru a le testa, voi crea câteva funcții care se înmulțesc și le decorează cu întârzieri diferite.

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

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

Acum, o să sun multiplica direct, precum și funcțiile mai lentă și măsurarea timpului.

"timpul de import pentru Python

funcția = [multiplicare, slow_multiply, slower_multiply] pentru funcțiile f: start = time.time () f (6, 7) print f, time.time ()

Aici este rezultatul:

neted 42 1.59740447998e-05 42 1.00477004051 42 3.00489807129

După cum puteți vedea, multiplicarea originală nu a durat aproape niciodată, iar versiunile mai lent au fost într-adevăr întârziate în funcție de durata de execuție minimă.

Un alt fapt interesant este că funcția decorată executată este ambalajul, ceea ce are sens dacă urmați definiția decorului. Dar asta ar putea fi o problemă, mai ales dacă avem de-a face cu decoratori de stivă. Motivul este că mulți decoratori își inspectează, de asemenea, intrarea lor și pot verifica numele, semnătura și argumentele. Următoarele secțiuni vor explora această problemă și vor oferi sfaturi pentru cele mai bune practici.

Obiecte Decoratoare

De asemenea, puteți utiliza obiecte ca decoratoare sau pentru a returna obiecte de la decoratorii dvs. Singura cerință este că aceștia au a __apel__() metodă, astfel încât acestea să poată fi sunrise. Iată un exemplu pentru un decorator bazat pe obiecte care numără de câte ori se numește funcția țintă:

(de exemplu, de exemplu, de exemplu, de exemplu, args, ** kwargs)

Aici este în acțiune:

"python @Counter def bbb (): tipăriți 'bbb'

bbb () bbb

bbb () bbb

bbb () bbb

print bbb.called 3 "

Alegerea între decoratori bazați pe funcții și obiect

Aceasta este cea mai mare parte o chestiune de preferință personală. Funcțiile născute și funcțiile de închidere asigură toată gestionarea de stat oferită de obiecte. Unii oameni se simt mai acasă cu clase și obiecte.

În următoarea secțiune, voi discuta decoratori bine-comportați, iar decoratorii pe bază de obiect își vor lua puțină muncă suplimentară pentru a se comporta bine.

Decoratori cu bine

Decoratorii cu scop general pot fi adesea stivuite. De exemplu:

python @ decorator_1 @ decorator_2 def foo (): imprima 'foo () aici'

Când stivuiești decoratorii, decoratorul exterior (decorator_1 în acest caz) va primi callablele returnate de decoratorul interior (decorator_2). Dacă decorator_1 depinde în vreun fel de numele, argumentele sau docstring-ul funcției originale și decorator_2 este implementat naiv, atunci decorator_2 nu va vedea informațiile corecte din funcția originală, ci doar apelul invocat de decorator_2.

De exemplu, aici este un decorator care verifică numele funcției sale țintă este cu litere mici:

Python def check_lowercase (f): def decorat (* args, ** kwargs): afirmați f.func_name == f.func_name.lower () f (* args, ** kwargs)

Să decorăm o funcție cu ea:

python @check_lowercase def Foo (): imprima 'Foo () aici'

Apelarea Foo () are ca rezultat o afirmație:

"simplu In [51]: Foo () - Trasare asertionError (cel mai recent apel ultimul)

în () ----> 1 Foo () (* args, ** kwargs): ----> 3 assert f.func_name == f.func_name.lower () 4 întoarcere (* args, ** kwargs) decorate "Dar dacă am stivuit ** decor_lowercase ** decorator peste un decorator ca ** hello_world ** care returnează o funcție imbricată numită 'decorat' rezultatul este foarte diferit:" python @check_lowercase @hello_world def Foo (): print ' Foo () aici "Foo () Hello World!" Decorul ** check_lowercase ** nu a ridicat o afirmație deoarece nu vedea numele funcției "Foo" .Aceasta este o problemă serioasă. este să păstrăm cât mai multe dintre atributele funcției originale posibil, să vedem cum se face.Voi crea acum un decorator de coajă, care pur și simplu numește intrarea sale apelabil, dar păstrează toate informațiile de la funcția de intrare: numele funcției, toate atributele sale (în cazul în care un decorator interior a adăugat unele atribute personalizate), și docstring sale ". (* args, ** kwargs): f (* args , ** kwargs) decorat .__ name__ = f .__ nume__ decorat .__ nume__ = f .__ modul__ decorat .__ dict__ = f .__ dict__ decorat .__ doc__ = f .__ doc__ returnat decorat "Acum, decoratori stivuite pe partea de sus a ** ** pastehrough decorator va funcționa ca și cum ar fi decorat direct funcția țintă. "python @check_lowercase @passthrough def Foo (): imprimați" Foo () aici "### Folosind @wraps Decorator Această funcție este atât de utilă încât biblioteca standard are o caracteristică specială decorator din modulul functools numit ['wraps'] (https://docs.python.org/2/library/functools.html#functools.wraps) pentru a ajuta la scrierea decoratorilor potriviti care functioneaza bine cu alti decoratori. Decorezi pur și simplu în interiorul decoratorului funcția returnată cu ** @ wrap (f) **. Vedeți cât de mult mai concis ** ** păstrează ** când folosește ** wraps **: "python from functools import wraps def passthrough (f): @wraps (f) def decorate (* args, ** kwargs) args, ** kwargs) întoarcere decorat "Vă recomandăm întotdeauna să îl folosiți cu excepția cazului în care decoratorul dvs. este proiectat să modifice unele dintre aceste atribute. ## Decorarea claselor de scris Decoratorii de clasă au fost introduși în Python 3.0. Ei operează pe o clasă întreagă. Un decorator de clasă este invocat atunci când o clasă este definită și înainte de crearea oricăror instanțe. Aceasta permite decoratorului de clasă să modifice aproape fiecare aspect al clasei. De obicei, veți adăuga sau decora mai multe metode. Să mergem direct la un exemplu fantezist: să presupunem că aveți o clasă numită "AwesomeClass" cu o mulțime de metode publice (metode al căror nume nu pornește de la un sublinieră ca __init__) și aveți o clasă de testare bazată pe unitatea de testare numită "AwesomeClassTest “. AwesomeClass nu este doar minunat, ci și foarte critic și doriți să vă asigurați că dacă cineva adaugă o nouă metodă la AwesomeClass, va adăuga și o metodă de testare corespunzătoare pentru AwesomeClassTest. Aici este AwesomeClass: "clasa python AwesomeClass: def awesome_1 (self): reveniți" minunat! " def awesome_2 (self): reveniți 'awesome! awesome!' Aici este AwesomeClassTest: "python de la unitateatesttest TestCase import, clasa principală AwesomeClassTest (TestCase): def test_awesome_1 (self): r = AwesomeClass ('awesome! awesome!', r) dacă __name__ == '__main__': main () "Acum, dacă cineva adaugă o metodă ** awesome_3 ** cu un bug, testele vor trece încă pentru că nu există nici un test care să apeleze ** awesome_3 **. Cum puteți să vă asigurați că există întotdeauna o metodă de testare pentru fiecare metodă publică? Ei bine, scrieți un decorator de clasă, desigur. Decodatorul de clasă @ensure_tests va decora AwesomeClassTest și se va asigura că fiecare metodă publică are o metodă de testare corespunzătoare. "Python def sure_tests (cls, target_class): test_methods = [m pentru m în cls. Dict__ dacă m.startswith ('test_' )] public_methods = [k pentru k, v în target_class .__ dict __. items () dacă este volatilă (v) și nu k.startswith ('_')] :] pentru m în metode de încercare] dacă set (metode test_)! = set (public_methods): ridicați RuntimeError ("Test / metode publice nepotrivite!") return cls "Acest lucru pare destul de bun, dar există o problemă. Decoratorii de clasă acceptă doar un singur argument: clasa decorată. Decodorul ensures_tests are nevoie de două argumente: clasa și clasa țintă. Nu am putut găsi o modalitate de a avea decoratori de clasă cu argumente similare cu decoratorii de funcții. Nu te teme. Python are funcția [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) doar pentru aceste cazuri. "Python @partial (sure_tests, target_class = AwesomeClass) clasa AwesomeClassTest (TestCase): def test_awesome_1 (auto): r = AwesomeClass () awesome_1 () self.assertEqual ('awesome! "r") __name__ == '__main__': main () "Rularea rezultatelor testelor cu succes deoarece toate metodele publice, ** awesome_1 ** și ** awesome_2 **, au metode de testare corespunzătoare, * test_awesome_1 ** și ** test_awesome_2 **. "-------------------------------------- -------------------------------- Ran 2 teste în 0.000s OK "Să adăugăm o nouă metodă ** awesome_3 ** fără un test corespunzător și să executați din nou testele. "clasa python AwesomeClass: def awesome_1 (self): reveniți" minunat! " def awesome_2 (self): întoarce-te "awesome! awesome!" Defineți din nou testele: "python3 a.py Traceback (cel mai recent apel ultimul): Fișierul" a.py ", linia 25, în" awesome_3 (self) .
Cod