Skriv dina egna Python Decorators

Översikt

I artikeln Deep Dive Into Python Decorators introducerade jag konceptet Python decorators, visade många coola dekoratörer och förklarade hur man använde dem.

I denna handledning visar jag dig hur man skriver egna dekoratörer. Som du ser kan du skriva ut dina egna dekoratörer och ge dig många möjligheter. Utan dekoratörer skulle de här egenskaperna kräva mycket felaktigt och repetitivt boilerplatta som klämmer fast din kod eller helt externa mekanismer som kodgenerering.

En snabb omgång om du inte vet någonting om dekoratörer. En dekoratör är en callable (funktion, metod, klass eller objekt med a ring upp() -metoden) som accepterar en callable som input och returnerar en callable som utgång. Vanligtvis gör det returnerade callable någonting före och / eller efter att anropet har ringts in. Du tillämpar dekoratorn genom att använda @ syntax. Massor av exempel kommer snart ...

Hello World Decorator

Låt oss börja med en "Hej värld!" dekoratör. Denna dekoratör kommer helt att ersätta alla dekorerade callable med en funktion som bara skriver ut 'Hello World!'.

python def hello_world (f): def dekorerade (* args, ** kwargs): skriv ut 'Hello World!' återvända inredda

Det är allt. Låt oss se det i aktion och förklara de olika bitarna och hur det fungerar. Antag att vi har följande funktion som accepterar två siffror och skriver ut sin produkt:

python def multiplicera (x, y): skriv ut x * y

Om du åberopar får du vad du förväntar dig:

multiplicera (6, 7) 42

Låt oss dekorera det med vårt Hej världen dekoratör genom att kommentera multiplicera fungera med @Hej världen.

python @hello_world def multiplicera (x, y): skriv ut x * y

Nu när du ringer multiplicera med några argument (inklusive felaktiga datatyper eller felaktigt antal argument) är resultatet alltid "Hello World!" tryckt.

"Python multiplicera (6, 7) Hello World!

multiplicera () Hello World!

multiplicera ("zzz") Hej världen! "

OK. Hur fungerar det? Den ursprungliga multipliceringsfunktionen ersattes helt av den nestade dekoreringsfunktionen inuti Hej världen dekoratör. Om vi ​​analyserar strukturen hos Hej världen dekoratorn så ser du att den accepterar ingången som kan ringas f (som inte används i denna enkla dekoratör), definierar den en kapslad funktion som heter dekorerad som accepterar en kombination av argument och sökordsargument (def dekorerade (* args, ** kwargs)), och slutligen returnerar den dekorerad fungera.

Skrivande funktion och metod dekoratörer

Det finns ingen skillnad mellan att skriva en funktion och en metodinstruktör. Dekoreringsdefinitionen kommer att vara densamma. Inmatningsangivelsen är antingen en vanlig funktion eller en bunden metod.

Låt oss verifiera det. Här är en dekoratör som bara skriver ut ingången som kan ringas och skriver innan den påkallas. Detta är mycket typiskt för en dekoratör att utföra en viss åtgärd och fortsätt genom att åberopa det ursprungliga kallbara.

python def print_callable (f): def dekorerad (* args, ** kwargs): skriv ut f, typ (f) returnera f (* args, ** kwargs)

Notera den sista raden som påkallar ingången som kan ringas på ett generiskt sätt och returnerar resultatet. Denna dekoratör är icke-påträngande i den meningen att du kan dekorera någon funktion eller metod i en arbetsapplikation, och ansökan fortsätter att fungera eftersom den inredda funktionen påkallar originalen och bara har en liten bieffekt före.

Låt oss se det i aktion. Jag ska dekorera både vår multiplicera funktion och en metod.

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

klass A (objekt): @print_callable def foo (själv): skriv ut 'foo () here "

När vi kallar funktionen och metoden, skrivs den utskrivbara ut och sedan utför de sin ursprungliga uppgift:

"Python multiplicera (6, 7) 42

A (). Foo () foo () här "

Dekoratörer med argument

Dekoratörer kan ta argument också. Denna förmåga att konfigurera en decorators funktion är mycket kraftfull och låter dig använda samma dekoratör i många sammanhang.

Antag att din kod är alldeles för snabb, och din chef ber dig att sakta ner det lite för att du gör de andra lagmedlemmarna dåliga. Låt oss skriva en dekoratör som mäter hur länge en funktion körs och om den körs på mindre än ett visst antal sekunder t, det väntar tills t sekunder löper ut och återkommer sedan.

Vad som är annorlunda nu är att dekoratören själv tar ett argument t som bestämmer minsta körtid, och olika funktioner kan dekoreras med olika minsta körtid. Du kommer också märka att när man introducerar dekoratörsargument krävs två nivåer av häckning:

"python importtid

def minimum_runtime (t): def dekorerade (f): def wrapper (args, ** kwargs): start = time.time () resultat = f (args, ** kwargs) runtime = time.time () - starta om körtid < t: time.sleep(t - runtime) return result return wrapper return decorated"

Låt oss packa upp det. Dekoratören själv-funktionen minimum_runtime tar ett argument t, vilket representerar minsta körtid för den dekorerade callable. Inmatningen kan ringas f var "nedtryckt" till det kapslade dekorerad funktionen och de inmatningsbara argumenten "pushed down" till ännu en nestad funktion omslag.

Den faktiska logiken sker inom omslag fungera. Starttiden är inspelad, den ursprungliga kan ringas f åberopas med sina argument, och resultatet lagras. Därefter kontrolleras körtiden, och om den är mindre än minimum t då sover den för resten av tiden och återkommer sedan.

För att testa det skapar jag ett par funktioner som kallar multiplicera och dekorera dem med olika förseningar.

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

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

Nu ska jag ringa multiplicera direkt liksom de långsammare funktionerna och mäta tiden.

"python importtid

funcs = [multiplicera, slow_multiply, slow_multiply] för f i funcs: start = time.time () f (6, 7) print f, time.time () - starta "

Här är utgången:

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

Som du kan se tog den ursprungliga multipliceringen nästan ingen tid, och de långsammare versionerna fördröjdes verkligen enligt den angivna minimiperioden.

Ett annat intressant faktum är att den utförda dekorerade funktionen är omslaget, vilket är meningsfullt om du följer definitionen på dekorerad. Men det kan vara ett problem, speciellt om vi har att göra med stack decorators. Anledningen är att många dekoratörer också inspekterar deras inmatningsbara och kontrollerar namn, signatur och argument. Följande avsnitt kommer att undersöka problemet och ge råd om bästa praxis.

Objekt Decorators

Du kan också använda föremål som dekoratörer eller returnera objekt från dina dekoratörer. Det enda kravet är att de har en __ring upp__() metod, så de kan kallas. Här är ett exempel på en objektbaserad dekoratör som räknar hur många gånger dess målfunktion heter:

python-klass Counter (objekt): def __init __ (själv, f): self.f = f self.called = 0 def __call __ (själv, * args, ** kwargs): self.called + = 1 returnera self.f (* args, ** kwargs)

Här är det i aktion:

"python @Counter def bbb (): skriv ut 'bbb'

bbb () bbb

bbb () bbb

bbb () bbb

skriv ut bbb.called 3 "

Välja mellan funktionsbaserade och objektbaserade dekoratörer

Det här är mestadels en fråga om personlig preferens. Nested funktioner och funktion stängningar ger all den statliga ledningen som objekten erbjuder. Vissa människor känner sig mer hemma med klasser och föremål.

I nästa avsnitt kommer jag att diskutera väluppförda dekoratörer, och objektbaserade dekoratörer tar lite extra arbete för att vara välskötta.

Välbetjänade dekoratörer

Allmänna dekoratörer kan ofta staplas. Till exempel:

python @ decorator_1 @ decorator_2 def foo (): print 'foo () här'

När du ställer in dekoratörer, kommer den yttre dekoratören (decorator_1 i det här fallet) att få det samtal som returneras av inredningsaren (decorator_2). Om decorator_1 beror på något sätt på namnet, är argumenter eller docstring av den ursprungliga funktionen och decorator_2 implementerad naivt, då kommer dekoratorn_2 att se inte se den korrekta informationen från den ursprungliga funktionen, men endast den samtal som returneras av decorator_2.

Till exempel, här är en dekoratör som verifierar sin målsfunktions namn är helt små bokstäver:

python def check_lowercase (f): def dekorerade (* args, ** kwargs): hävda f.func_name == f.func_name.lower () f (* args, ** kwargs)

Låt oss dekorera en funktion med det:

python @check_lowercase def Foo (): print 'Foo () här'

Ringa Foo () resulterar i ett påstående:

"plain In [51]: Foo () - AssertionError Traceback (senaste samtal sist)

i () ----> 1 Foo () i dekorerad (* args, ** kwargs) 1 def check_lowercase (f): 2 def dekorerad (* args, ** kwargs): ----> 3 assert f.func_name == f.func_name.lower dekorerade "men om vi staplar ** check_lowercase ** dekoratorn över en dekoratör som ** hello_world ** som returnerar en kapslad funktion som kallas" dekorerad "är resultatet väldigt annorlunda:" python @check_lowercase @hello_world def Foo (): print ' Foo () här "Foo () Hej världen!" ** check_lowercase ** decoratorn höjde inte en påstående eftersom den inte såg funktionsnamnet "Foo". Det här är ett allvarligt problem. är att behålla så mycket av attributen till den ursprungliga funktionen som möjligt. Låt oss se hur det är gjort. Jag ska nu skapa en skaldekorator som helt enkelt kallar sin inmatningsanrop, men behåller all information från inmatningsfunktionen: funktionsnamnet, alla dess attribut (om en inre dekoratör har lagt till några anpassade attribut) och dess doktrering. "python def passthrough (f): def dekorerad (* args, ** kwargs): f (* args , ** kwargs) dekorerad .__ name__ = f .__ name__ decorated____ name__ = f .__ module__ decorated.__ dict__ = f .__ dict__ decorated____ doc__ = f .__ doc__ return decorated "nu dekoratörer staplade på toppen av ** passthrough ** decorator kommer att fungera som om de dekorerade målfunktionen direkt. "python @check_lowercase @passthrough def Foo (): print 'Foo () här" ### Använda @wraps Decorator Denna funktion är så användbar att standardbiblioteket har en speciell dekoratör i functools-modulen som heter ['wraps'] (https://docs.python.org/2/library/functools.html#functools.wraps) för att hjälpa till att skriva ordentliga dekoratörer som fungerar bra med andra dekoratörer. Du dekorerar bara inuti din dekoratör den returnerade funktionen med ** @ wraps (f) **. Se hur mycket mer kortfattat ** passthrough ** ser ut när man använder ** wraps **: "python från functools import wraps def passthrough (f): @wraps (f) def dekorerade (* args, ** kwargs): f (* args, ** kwargs) returneras dekorerade "Jag rekommenderar starkt att alltid använda den om inte din dekoratör är utformad för att modifiera några av dessa attribut. ## Skrivande klassdekoratorer Klassdekoratorer introducerades i Python 3.0. De arbetar på en hel klass. En klassdekorerare åberopas när en klass definieras och innan några instanser skapas. Det gör att klassens dekoratör kan ändra ganska mycket varje aspekt av klassen. Vanligtvis lägger du till eller dekorerar flera metoder. Låt oss hoppa direkt in i ett fint exempel: anta att du har en klass som heter 'AwesomeClass' med en massa offentliga metoder (metoder vars namn inte börjar med ett understreck som __init__) och du har en testbaserad testkurs som heter 'AwesomeClassTest '. AwesomeClass är inte bara fantastisk, men också mycket kritisk, och du vill se till att om någon lägger till en ny metod till AwesomeClass, lägger de också till en motsvarande testmetod till AwesomeClassTest. Här är AwesomeClass: "python-klassen AwesomeClass: def awesome_1 (själv): returnera" awesome! " Def awesome_2 (self): return "awesome! awesome!" Här är AwesomeClassTest: "python från unittest import TestCase, huvudklass AwesomeClassTest (TestCase): def test_awesome_1 (själv): r = AwesomeClass (). awesome_1 () self.assertEqual ("awesome!", r) def test_awesome_2 (själv): r = AwesomeClass (). awesome_2 () self.assertEqual ('awesome! awesome!', r) om __name__ == '__main__': main Om någon lägger till en ** awesome_3 ** -metod med ett fel, kommer testen fortfarande att passera eftersom det inte finns något test som kallar ** awesome_3 **. Hur kan du se till att det alltid finns en testmetod för alla offentliga metoder? Jo, du skriver naturligtvis en klassens dekoratör. Klassificeringsenheten @ensure_tests kommer att dekorera AwesomeClassTest och försäkra sig om att alla offentliga metoder har en motsvarande testmetod. "Python def ensure_tests (cls, target_class): test_methods = [m för m in cls .__ dict__ om m.startswith ('test_' )] public_methods = [k för k, v i mål_class .__ dict __. items () om callable (v) och inte k.startswith ('_')] # Strip 'test_' prefix från testmetod namn test_methods = [m [5 :] för m i test_methods] om set (test_methods)! = set (public_methods): höja RuntimeError ('Test / public methods mismatch!') returnera cls "Det ser ganska bra ut, men det finns ett problem. Klassens dekoratörer accepterar bara ett argument: den inredda klassen. Försäkringsutredaren har två argument: klassen och målklassen. Jag kunde inte hitta ett sätt att få klassen dekoratörer med argument som liknar funktionen dekoratörer. Var inte rädd. Python har funktionen [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) bara för dessa fall. "Python @partial (secure_tests, target_class = AwesomeClass) klass AwesomeClassTest (TestCase): def test_awesome_1 (själv): r = AwesomeClass (). Awesome_1 () self.assertEqual ('awesome!', R) def test_awesome_2 (själv): r = AwesomeClass (). Awesome_2 () self.assertEqual (' awesome! ", r) om __name__ == '__main__': main ()" Kör testresultaten framgångsrikt eftersom alla offentliga metoder, ** awesome_1 ** och ** awesome_2 **, har motsvarande testmetoder, * * test_awesome_1 ** och ** test_awesome_2 **. "-------------------------------------- -------------------------------- Ran 2 test i 0.000s OK "Låt oss lägga till en ny metod ** awesome_3 ** utan ett motsvarande test och kör testen igen. "python klass AwesomeClass: def awesome_1 (själv): returnera" awesome! " def awesome_2 (sig själv): returnera "awesome! awesome!" Def awesome_3 (self): return "awesome! awesome! awesome!" Om du kör testen igen leder resultatet till följande: "python3 a.py Traceback (senaste samtal senast): Filen" a.py ", rad 25, i .