Skriv dine egne Python Decorators

Oversikt

I artikkelen Deep Dive Into Python Decorators presenterte jeg begrepet Python decorators, demonstrerte mange kule dekoratører, og forklarte hvordan de skulle brukes.

I denne veiledningen vil jeg vise deg hvordan du skriver dine egne dekoratører. Som du vil se, kan du skaffe deg mye kontroll og lage mange muligheter. Uten dekoratører ville disse evnene kreve mye feilaktig og repeterende boilerplate som clutters koden din eller helt eksterne mekanismer som kodegenerering.

En rask omtale hvis du ikke vet noe om dekoratører. En dekoratør er en callable (funksjon, metode, klasse eller objekt med a anrop() metode) som aksepterer en callable som input og returnerer en callable som utgang. Vanligvis gjør den returnerte callable noe før og / eller etter at innringingen er mulig. Du bruker dekoratøren ved å bruke @ syntaks. Massevis av eksempler kommer snart ...

Hello World Decorator

La oss starte med en "Hei verden!" dekoratør. Denne dekoratøren vil helt erstatte alle dekorert callable med en funksjon som bare skriver ut 'Hello World!'.

python def hello_world (f): def dekorert (* args, ** kwargs): skriv ut 'Hello World!' returnere dekorert

Det er det. La oss se det i aksjon og forklare de forskjellige stykkene og hvordan det fungerer. Anta at vi har følgende funksjon som aksepterer to tall og skriver ut deres produkt:

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

Hvis du påberoper, får du det du forventer:

multiplisere (6, 7) 42

La oss dekorere den med vår Hei Verden dekoratør ved å annotere multiplisere fungere med @Hei Verden.

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

Nå, når du ringer multiplisere med eventuelle argumenter (inkludert feil datatyper eller feil antall argumenter), er resultatet alltid 'Hello World!' skrevet ut.

"Python multipliserer (6, 7) Hello World!

multiplisere () Hello World!

multiplisere ('zzz') Hei verden! "

OK. Hvordan virker det? Den opprinnelige multipliseringsfunksjonen ble helt erstattet av den nestede dekorerte funksjonen inne i Hei Verden dekoratør. Hvis vi analyserer strukturen til Hei Verden dekorator så ser du at den aksepterer inntastingen som kan kalles f (som ikke er brukt i denne enkle dekoratøren), definerer den en nestet funksjon som kalles dekorert som aksepterer en kombinasjon av argumenter og søkeord argumenter (def dekorert (* args, ** kwargs)), og til slutt returnerer den dekorert funksjon.

Skrivefunksjon og metodeinnredning

Det er ingen forskjell mellom å skrive en funksjon og en metodeutsmyker. Dekoratørdefinisjonen vil være den samme. Inngangskallet vil enten være en vanlig funksjon eller en bundet metode.

La oss bekrefte det. Her er en dekoratør som bare skriver inn innkallingen som kan ringes og skriver før den påkaller seg. Dette er veldig typisk for en dekoratør å utføre en handling og fortsette ved å påkalle den opprinnelige callable.

python def print_callable (f): def dekorert (* args, ** kwargs): print f, type (f) returnere f (* args, ** kwargs)

Merk den siste linjen som påkaller inntastingen som kan ringes inn generisk, og returnerer resultatet. Denne dekoratøren er ikke-påtrengende i den forstand at du kan dekorere hvilken som helst funksjon eller metode i et arbeidsapplikasjon, og søknaden vil fortsette å fungere fordi den dekorerte funksjonen påkaller originalen og bare har en liten bivirkning før.

La oss se det i aksjon. Jeg skal dekorere både vår multiplikasjonsfunksjon og en metode.

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

klasse A (objekt): @print_callable def foo (selv): skriv ut 'foo () her "

Når vi kaller funksjonen og metoden, skrives den utringbare ut, og deretter utfører de sin opprinnelige oppgave:

"Python Multiply (6, 7) 42

A (). Foo () foo () her "

Dekoratører med argumenter

Dekoratører kan også ta argumenter. Denne muligheten til å konfigurere driften av en dekoratør er meget kraftig og lar deg bruke den samme dekoratøren i mange sammenhenger.

Anta at koden din er altfor fort, og sjefen din spør deg om å bremse den litt, fordi du gjør de andre lagmedlemmene til å se dårlig ut. La oss skrive en dekoratør som måler hvor lenge en funksjon kjører, og hvis den kjører på mindre enn et visst antall sekunder t, det vil vente til t sekunder utløper og deretter returnere.

Det som er forskjellig nå er at dekoratøren selv tar et argument t som bestemmer minimum kjøretid, og forskjellige funksjoner kan dekoreres med forskjellige minimum kjøretider. Også, vil du legge merke til at når du innfører dekorator argumenter, er to nivåer av nesting kreves:

"python importtid

def minimum_runtime (t): def dekorert (f): def wrapper (args, ** kwargs): start = time.time () resultat = f (args, ** kwargs) runtime = time.time () - start hvis kjøretid < t: time.sleep(t - runtime) return result return wrapper return decorated"

La oss pakke ut det. Dekoratøren selv-funksjonen minimum_runtime tar et argument t, som representerer minimum kjøretid for dekorert callable. Inngangskallet f ble "presset ned" til den nestede dekorert funksjon, og de innringbare argumentene ble "presset ned" til enda en nestet funksjon wrapper.

Den faktiske logikken finner sted inne i wrapper funksjon. Starttiden er registrert, den opprinnelige callable f er påkalt med sine argumenter, og resultatet er lagret. Deretter kontrolleres kjøretiden, og hvis den er mindre enn minimum t så sover den for resten av tiden og returnerer deretter.

For å teste det, vil jeg opprette et par funksjoner som kaller multiplisere og dekorere dem med forskjellige forsinkelser.

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

@minimum_runtime (3) def slow_multiply (x, y): multiply (x, y) "

Nå skal jeg ringe multiplisere direkte så vel som de langsommere funksjonene og måle tiden.

"python importtid

funcs = [multiply, slow_multiply, tregere_multiply] for f i funcs: start = time.time () f (6, 7) print f, time.time () - start "

Her er utgangen:

vanlig 42 1,59740447998e-05 42 1.00477004051 42 3,00489807129

Som du ser, tok den opprinnelige multiplikasjonen nesten ingen tid, og de langsommere versjoner var faktisk forsinket i henhold til den angitte minimumsperioden.

Et annet interessant faktum er at den utførte dekorerte funksjonen er omslaget, noe som gir mening hvis du følger definisjonen av dekorert. Men det kan være et problem, spesielt hvis vi har å gjøre med stakkens dekoratører. Årsaken er at mange dekoratører også inspiserer deres innringbare og kontrollerer navn, signatur og argumenter. Følgende avsnitt vil undersøke dette problemet og gi råd om beste praksis.

Objekt Dekoratører

Du kan også bruke gjenstander som dekoratører eller returnere gjenstander fra dekoratørene dine. Det eneste kravet er at de har en __anrop__() metode, slik at de kan kalles. Her er et eksempel på en objektbasert dekoratør som teller hvor mange ganger målfunksjonen heter:

python klasse Counter (objekt): def __init __ (selv, f): self.f = f self.called = 0 def __call __ (selv, * args, ** kwargs): self.called + = 1 returner self.f (* args, ** kwargs)

Her er det i aksjon:

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

bbb () bbb

bbb () bbb

bbb () bbb

skriv ut bbb.called 3 "

Velge mellom funksjonsbaserte og objektbaserte dekoratører

Dette er for det meste et spørsmål om personlig preferanse. Nestede funksjoner og funksjonstenger gir all statlig ledelse som objekter tilbyr. Noen føler seg mer hjemme med klasser og gjenstander.

I neste avsnitt skal jeg diskutere veloppdragen dekoratører, og objektbaserte dekoratører tar litt ekstra arbeid for å være friskopptatt.

Velbehandlede dekoratører

Generelle dekoratører kan ofte stables. For eksempel:

python @ decorator_1 @ decorator_2 def foo (): print 'foo () her'

Ved stablering av dekoratører, vil den ytre dekoratøren (decorator_1 i dette tilfellet) motta det kallbare returneres av den innvendige dekoratøren (decorator_2). Hvis decorator_1 avhenger på en eller annen måte på navnet, blir argumenter eller doktrering av den opprinnelige funksjonen og decorator_2 implementert naivt, så vil decorator_2 ikke se den riktige informasjonen fra den opprinnelige funksjonen, men bare den ringbare returnerte av decorator_2.

For eksempel, her er en dekoratør som bekrefter at målfunksjonens navn er helt små:

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

La oss dekorere en funksjon med det:

python @check_lowercase def Foo (): print 'Foo () her'

Ringe Foo () resulterer i et påstand:

"plain In [51]: Foo () - AssertionError Traceback (siste samtalen sist)

i () ----> 1 Foo () i dekorert (* args, ** kwargs) 1 def check_lowercase (f): 2 def dekorerte (* args, ** kwargs): ----> 3 assert f.func_name == f.func_name.lower dekorert "Men hvis vi stabler ** decor_lowercase ** dekoratøren over en dekoratør som ** hello_world ** som returnerer en nestet funksjon kalt" dekorert ", er resultatet veldig annerledes:" python @check_lowercase @hello_world def Foo (): print ' Foo () her 'Foo () Hello World! "** Check_lowercase ** decorator ikke hevdet en påstand fordi den ikke fikk se funksjonsnavnet' Foo '. Dette er et alvorlig problem. er å bevare så mye av egenskapene til den opprinnelige funksjonen som mulig. La oss se hvordan det er gjort. Jeg skal nå opprette en shell-dekoratør som bare kaller inntastingen sin, men beholder all informasjon fra inngangsfunksjonen: funksjonsnavnet, alle dens attributter (i tilfelle en indre dekoratør la til noen egendefinerte attributter) og dens doktrering. "python def passthrough (f): def dekorert (* args, ** kwargs): f (* args , ** kwargs) dekorert .__ name__ = f .__ name__ decorated____ name__ = f .__ module__ decorated.__ dict__ = f .__ dict__ decorated____ doc__ = f .__ doc__ return decorated "nå dekoratører stablet på toppen av ** passthrough ** decorator vil fungere som om de dekorerte målfunksjonen direkte. "python @check_lowercase @passthrough def Foo (): print 'Foo () her" ### Bruke @wraps Decorator Denne funksjonaliteten er så nyttig at standardbiblioteket har en spesiell dekoratør i functools-modulen kalt ['wraps'] (https://docs.python.org/2/library/functools.html#functools.wraps) for å skrive ordentlig dekoratører som fungerer godt sammen med andre dekoratører. Du dekorerer bare innvendig dekoratøren din med den returnerte funksjonen med ** @ wraps (f) **. Se hvor mye mer konsistent ** passthrough ** ser når du bruker ** wraps **: "python fra functools import wraps def passthrough (f): @wraps (f) def dekorert (* args, ** kwargs): f (* args, ** kwargs) returnert dekorert "Jeg anbefaler at du alltid bruker det, med mindre dekoratøren din er designet for å endre noen av disse egenskapene. ## Skrive klassedekorer Klassekunstnere ble introdusert i Python 3.0. De opererer på en hel klasse. En klassen dekorator er påkalt når en klasse er definert og før noen tilfeller blir opprettet. Det gjør at klassen dekoratøren kan endre stort sett alle aspekter av klassen. Vanligvis vil du legge til eller dekorere flere metoder. La oss hoppe rett inn i et fancy eksempel: anta at du har en klasse som heter 'AwesomeClass' med en mengde offentlige metoder (metoder hvis navn ikke starter med en underskrift som __init__) og du har en unittests-basert testklasse kalt 'AwesomeClassTest '. AwesomeClass er ikke bare fantastisk, men også veldig kritisk, og du vil sørge for at hvis noen legger til en ny metode til AwesomeClass, legger de også til en tilsvarende testmetode til AwesomeClassTest. Her er AwesomeClass: "python klasse AwesomeClass: def awesome_1 (selv): returnere" awesome! " Def awesome_2 (self): return awesome! awesome! Her er AwesomeClassTest: "python fra unittest import TestCase, hovedklasse AwesomeClassTest (TestCase): def test_awesome_1 (selv): r = AwesomeClass (). awesome_1 () self.assertEqual ('awesome!', r) def test_awesome_2 (selv): r = AwesomeClass (). awesome_2 () self.assertEqual ('awesome! awesome!', r) hvis __name__ == '__main__': main hvis noen legger til en ** awesome_3 ** -metode med en feil, vil testene fortsatt passere fordi det ikke er noen test som kaller ** awesome_3 **. Hvordan kan du sikre at det alltid finnes en testmetode for alle offentlige metoder? Vel, du skriver selvfølgelig en klassekunstner. @Ensure_tests klassen dekoratøren vil dekorere AwesomeClassTest og sørge for at alle offentlige metoder har en tilsvarende testmetode. "Python def sikre_tests (cls, target_class): test_methods = [m for m in cls .__ dict__ hvis m.startswith ('test_' )] public_methods = [k for k, v i target_class .__ dict __. items () hvis callable (v) og ikke k.startswith ('_')] # Strip 'test_' prefiks fra testmetodenavn test_methods = [m [5 :] for m i test_methods] hvis set (test_methods)! = set (public_methods): heve RuntimeError ('Test / Public Method mismatch!') Return Cls "Dette ser ganske bra ut, men det er ett problem. Klasse dekoratører godta bare ett argument: den dekorerte klassen. Den sikre_tests dekoratøren trenger to argumenter: klassen og målklassen. Jeg kunne ikke finne en måte å ha klassen dekoratører med argumenter som ligner på funksjon dekorere. Ha ingen frykt. Python har funksjonen [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) for disse tilfellene. "Python @partial (secure_tests, target_class = AwesomeClass) klasse AwesomeClassTest (TestCase): def test_awesome_1 (selv): r = AwesomeClass (). Awesome_1 () self.assertEqual ('awesome!', R) def test_awesome_2 (selv): r = AwesomeClass (). Awesome_2 () self.assertEqual (' kjempebra! ', r) hvis __name__ ==' __main__ ': main () "Kjører testresultatene i suksess fordi alle offentlige metoder, ** awesome_1 ** og ** awesome_2 **, har tilsvarende testmetoder, * * test_awesome_1 ** og ** test_awesome_2 **. "-------------------------------------- -------------------------------- Ran 2 tester i 0.000s OK "La oss legge til en ny metode ** awesome_3 ** uten en tilsvarende test og kjøre testene igjen. "python klasse AwesomeClass: def awesome_1 (selv): return" awesome! " Def awesome_2 (selvtillit): Return "awesome! awesome!" def awesome_3 (selv): return "awesome! awesome! awesome!" Kjører testene igjen resulterer i følgende utgang: "python3 a.py Traceback (siste anrop sist): Fil" a.py ", linje 25, i .