Schrijf je eigen Python-decorateurs

Overzicht

In het artikel Deep Dive Into Python Decorators, introduceerde ik het concept van Python-decorateurs, demonstreerde veel coole decorateurs en legde uit hoe ze te gebruiken.

In deze tutorial laat ik je zien hoe je je eigen decorateurs kunt schrijven. Zoals je zult zien, geeft het schrijven van je eigen decorateurs je veel controle en maakt je veel mogelijkheden. Zonder decorateurs zou die functionaliteit veel foutgevoelige en repetitieve boilerplate vereisen die uw code verstikt of volledig externe mechanismen zoals het genereren van codes.

Een korte samenvatting als je niets over decorateurs weet. Een binnenhuisarchitect is een opvraagbare (functie, methode, klasse of object met een telefoontje() methode) die een opvraagbare waarde als invoer accepteert en een opvraagbare waarde als uitvoer retourneert. Doorgaans doet de geretourneerde opvraag iets vóór en / of na het aanroepen van de invoer die kan worden gecalld. U past de decorateur toe door @ te gebruiken syntaxis. Veel voorbeelden komen binnenkort ...

De Hello World Decorator

Laten we beginnen met een 'Hallo wereld!' decorateur. Deze inrichter vervangt alle aangeklede versies van de callable door een functie die alleen 'Hello World!'.

python def hello_world (f): def decorated (* args, ** kwargs): print 'Hello World!' terug versierd

Dat is het. Laten we het in actie zien en vervolgens de verschillende stukken uitleggen en hoe het werkt. Stel dat we de volgende functie hebben die twee nummers accepteert en hun product afdrukt:

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

Als je een beroep doet, krijg je wat je verwacht:

vermenigvuldigen (6, 7) 42

Laten we het versieren met onze Hallo Wereld decorateur door annotatie van de vermenigvuldigen functie met @Hallo Wereld.

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

Nu, wanneer u belt vermenigvuldigen met alle argumenten (inclusief verkeerde gegevenstypes of verkeerd aantal argumenten), is het resultaat altijd 'Hallo wereld!' gedrukt.

"Python Multiply (6, 7) Hallo wereld!

vermenigvuldigen () Hallo wereld!

vermenigvuldigen ('zzz') Hallo wereld! "

OK. Hoe werkt het? De oorspronkelijke vermenigvuldigingsfunctie werd volledig vervangen door de geneste gedecoreerde functie binnen de Hallo Wereld decorateur. Als we de structuur van de Hallo Wereld decorateur dan zul je zien dat het de invoer accepteert die kan worden ingewisseld f (die niet wordt gebruikt in deze eenvoudige decorateur), definieert het een geneste functie genaamd versierd die elke combinatie van argumenten en zoekwoordargumenten accepteert (def decorated (* args, ** kwargs)) en tenslotte retourneert het de versierd functie.

Schrijffunctie en methode Decorators

Er is geen verschil tussen het schrijven van een functie en een methode-decorateur. De definitie van de decorateur is hetzelfde. De invoerbare opvraagbare waarde zal een reguliere functie of een gebonden methode zijn.

Laten we dat controleren. Hier is een decorateur die alleen de invoer die kan worden gecallteerd en het type afdrukt voordat deze wordt aangeroepen. Dit is typisch voor een decorateur om wat actie uit te voeren en door te gaan met het oproepen van de originele opvraagbare actie.

python def print_callable (f): def decorated (* args, ** kwargs): print f, type (f) return f (* args, ** kwargs) return ingericht

Let op de laatste regel die de invoer opvraagbaar op een generieke manier oproept en het resultaat retourneert. Deze binnenhuisarchitect is niet-opdringerig in die zin dat je elke functie of methode in een werkende toepassing kunt verfraaien, en de applicatie zal blijven werken omdat de versierde functie het origineel oproept en slechts een beetje neveneffect heeft voordat.

Laten we het in actie zien. Ik versier onze beide vermenigvuldigingsfuncties en een methode.

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

klasse A (object): @print_callable def foo (self): print hier 'foo ()'

Wanneer we de functie en de methode aanroepen, wordt de opvraagbaarheid afgedrukt en voeren ze vervolgens hun oorspronkelijke taak uit:

"Python vermenigvuldigen (6, 7) 42

A (). Foo () foo () hier "

Decorateurs met argumenten

Decorateurs kunnen ook argumenten aanvoeren. Deze mogelijkheid om de werking van een binnenhuisarchitect in te stellen is zeer krachtig en stelt u in staat in meerdere contexten dezelfde decorateur te gebruiken.

Stel dat je code veel te snel is en je baas vraagt ​​je om het een beetje te vertragen omdat je de andere teamleden er slecht uit laat zien. Laten we een decorateur schrijven die meet hoe lang een functie actief is en of deze in minder dan een bepaald aantal seconden wordt uitgevoerd t, het zal wachten tot t seconden verlopen en dan terugkeren.

Wat nu anders is, is dat de binnenhuisarchitect zelf een ruzie maakt t dat bepaalt de minimale looptijd en verschillende functies kunnen worden ingericht met verschillende minimale looptijden. Ook zul je merken dat er bij het introduceren van decoratorargumenten twee niveaus van nesten zijn vereist:

"python import tijd

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

Laten we het uitpakken. De binnenhuisarchitect zelf - de functie minimum_runtime neemt een argument t, Dit is de minimale looptijd voor de versierde opvraagbare telefoon. De invoer kan worden gecalld f werd "naar beneden gedrukt" naar de geneste versierd functie, en de argumenten voor opvraagbare invoer zijn "naar beneden gedrukt" naar nog een andere geneste functie wikkel.

De eigenlijke logica vindt plaats binnen de wikkel functie. De starttijd wordt vastgelegd, de oorspronkelijke opvraagbare f wordt aangeroepen met zijn argumenten en het resultaat wordt opgeslagen. Vervolgens wordt de runtime gecontroleerd en als deze minder is dan het minimum t dan slaapt het voor de rest van de tijd en keert dan terug.

Om het te testen, maak ik een aantal functies die vermenigvuldigd worden genoemd en versier ze met verschillende vertragingen.

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

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

Ik bel nu vermenigvuldigen direct evenals de langzamere functies en meet de tijd.

"python import tijd

funcs = [vermenigvuldig, langzaam_veelvoudig, langzamer_veelvoud] voor f in functies: start = time.time () f (6, 7) print f, time.time () - start "

Dit is de uitvoer:

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

Zoals u kunt zien, nam de oorspronkelijke vermenigvuldiging bijna geen tijd in beslag, en de langzamere versies werden inderdaad vertraagd volgens de opgegeven minimale looptijd.

Een ander interessant feit is dat de uitgevoerde gedecoreerde functie de verpakking is, wat logisch is als je de definitie van de versiering volgt. Maar dat kan een probleem zijn, vooral als we te maken hebben met stackdecorateurs. De reden is dat veel decorateurs ook hun invoer opvraagbaar inspecteren en de naam, handtekening en argumenten controleren. In de volgende secties wordt dit probleem onderzocht en wordt advies gegeven voor de beste werkwijzen.

Object Decorators

U kunt ook objecten als decorateur gebruiken of objecten van uw decorateurs retourneren. De enige vereiste is dat ze een __call __ () methode, zodat ze opvraagbaar zijn. Hier is een voorbeeld voor een op objecten gebaseerde decorateur die telt hoe vaak de doelfunctie wordt aangeroepen:

python-klasse Teller (object): def __init __ (zelf, f): self.f = f self.called = 0 def __call __ (zelf, * args, ** kwargs): self.called + = 1 return self.f (* args, ** kwargs)

Hier is het in actie:

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

bbb () bbb

bbb () bbb

bbb () bbb

print bbb.called 3 "

Kiezen tussen functiegebaseerde en object-gebaseerde decorateurs

Dit is meestal een kwestie van persoonlijke voorkeur. Geneste functies en functiesluitingen bieden alle statusbeheer dat objecten bieden. Sommige mensen voelen zich meer thuis bij lessen en objecten.

In de volgende sectie zal ik discussiëren over goed opgevoede binnenhuisarchitecten, en objectgebaseerde decorateurs nemen wat extra werk om zich goed te gedragen.

Welgemanierde decorateurs

Decorateurs voor algemene doeleinden kunnen vaak worden gestapeld. Bijvoorbeeld:

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

Bij het stapelen van decorateurs ontvangt de buitendeur (decorator_1 in dit geval) de opvraagbare retouraanroep van de binnenhuisarchitect (decorator_2). Als decorator_1 op de een of andere manier afhankelijk is van de naam, argumenten of docstring van de oorspronkelijke functie en decorator_2 naïef is geïmplementeerd, zal decorator_2 de juiste informatie niet zien van de oorspronkelijke functie, maar alleen de opvraagbare geretourneerd door decorator_2.

Hier is bijvoorbeeld een binnenhuisarchitect die controleert of de naam van zijn doelfunctie allemaal in kleine letters is:

python def check_lowercase (f): def decorated (* args, ** kwargs): assert f.func_name == f.func_name.lower () f (* args, ** kwargs) return ingericht

Laten we er een functie mee decoreren:

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

Het aanroepen van Foo () resulteert in een bewering:

"plain In [51]: Foo () - AssertionError Traceback (laatste oproep laatste)

in () ----> 1 Foo () in versierd (* args, ** kwargs) 1 def check_lowercase (f): 2 def gedecoreerd (* args, ** kwargs): ----> 3 assert f.func_name == f.func_name.lower () 4 return gedecoreerd "Maar als we de decorateur ** check_lowercase ** op een decorateur zoals ** hello_world ** stapelen die een geneste functie retourneert die 'decorated' wordt genoemd, is het resultaat heel anders: 'python @check_lowercase @hello_world def Foo (): print' Foo () hier 'Foo () Hallo wereld!' De ** check_lowercase ** -decorator heeft geen bewering gemaakt omdat deze de functienaam 'Foo' niet heeft gezien. Dit is een serieus probleem. Het juiste gedrag voor een decorateur is om zo veel mogelijk van de eigenschappen van de originele functie te behouden. Laten we kijken hoe het gedaan is. Ik zal nu een shell-decorateur maken die zijn invoer eenvoudig opvraagt, maar alle informatie uit de invoerfunctie behoudt: de functienaam, al zijn attributen (in het geval een innerlijke decorateur enkele aangepaste attributen heeft toegevoegd), en zijn docstring. "python def passthrough (f): def decorated (* args, ** kwargs): f (* args , ** kwargs) ingericht .__ naam__ = f .__ naam__ ingericht .__ naam__ = f .__ module__ ingericht .__ dict__ = f .__ dict__ ingericht .__ doc__ = f .__ doc__ geretourneerd gedecoreerd "Nu hebben decorateurs gestapeld bovenop de ** passthrough ** decorateur werkt net alsof ze de doelfunctie direct hebben ingericht. "python @check_lowercase @passthrough def Foo (): print hier 'Foo ()' ### De @wraps Decorator gebruiken Deze functionaliteit is zo handig dat de standaardbibliotheek een speciale functie heeft decorateur in de module functools genaamd ['wraps'] (https://docs.python.org/2/library/functools.html#functools.wraps) om te helpen bij het schrijven van de juiste decorateurs die goed samenwerken met andere decorateurs. Je decoreert eenvoudig in je binnenhuisarchitect de geretourneerde functie met ** @ wraps (f) **. Zie hoe veel beknopter ** passthrough ** er uitziet bij het gebruik van ** wraps **: "python van functools import-wraps def passthrough (f): @wraps (f) def decorated (* args, ** kwargs): f (* args, ** kwargs) retour gedecoreerd "Ik beveel het ten zeerste aan om het altijd te gebruiken, tenzij je decorateur is ontworpen om enkele van deze kenmerken te wijzigen. ## Writing Class Decorators Class-decorateurs werden geïntroduceerd in Python 3.0. Ze werken op een hele klas. Een klassendecorator wordt aangeroepen wanneer een klasse wordt gedefinieerd en voordat instanties worden gemaakt. Op die manier kan de klassendecorator vrijwel elk aspect van de klas aanpassen. Meestal zult u meerdere methoden toevoegen of versieren. Laten we meteen beginnen met een fraai voorbeeld: stel dat je een klasse hebt met de naam 'AwesomeClass' met een aantal openbare methoden (methoden waarvan de naam niet begint met een onderstrepingsteken zoals __init__) en je hebt een testklasse op unittests genaamd 'AwesomeClassTest '. AwesomeClass is niet alleen geweldig, maar ook erg kritisch en je wilt ervoor zorgen dat als iemand een nieuwe methode toevoegt aan AwesomeClass ook een bijbehorende testmethode wordt toegevoegd aan AwesomeClassTest. Hier is de AwesomeClass: "python class AwesomeClass: def awesome_1 (self): return 'awesome!' def awesome_2 (self): return 'awesome! awesome!' Hier is de AwesomeClassTest: "python van unittest import TestCase, hoofdklasse AwesomeClassTest (TestCase): def test_awesome_1 (zelf): r = AwesomeClass (). awesome_1 () self.assertEqual ('geweldig!', r) def test_awesome_2 (self): r = AwesomeClass (). awesome_2 () self.assertEqual ('awesome! awesome!', r) if __name__ == '__main__': main () "Nu, als iemand een ** awesome_3 ** -methode met een bug toevoegt, zullen de tests nog steeds slagen omdat er geen test is die ** awesome_3 ** aanroept. Hoe kunt u ervoor zorgen dat er altijd een testmethode is voor elke openbare methode? Nou, je schrijft natuurlijk een klassendecorateur. De classensator @ensure_tests decoreert de AwesomeClassTest en zorgt ervoor dat elke openbare methode een overeenkomende testmethode heeft. "Python def sure_tests (cls, target_class): test_methods = [m voor m in cls .__ dict__ if m.startswith ('test_' )] public_methods = [k for k, v in target_class .__ dict __. items () if callable (v) and not k.startswith ('_')] # Strip 'test_' voorvoegsel van namen van testmethoden test_methods = [m [5 :] voor m in testmethodes] indien ingesteld (testmethoden)! = ingesteld (public_methods): raise RuntimeError ('Test / publieke methoden komen niet overeen!') return cls "Dit ziet er goed uit, maar er is één probleem. Klasse decorateurs accepteren slechts één argument: de gedecoreerde klasse. De decorateur_progres_tests heeft twee argumenten nodig: de klasse en de doelklasse. Ik kon geen manier vinden om klassenschildpadden te gebruiken met argumenten die vergelijkbaar zijn met functie-decorateurs. Geen schrik hebben. Python heeft de functie [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) alleen voor deze gevallen. "Python @partial (guarant_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 (' geweldig! geweldig! ', r) if __name__ ==' __main__ ': main () "De testresultaten met succes uitvoeren omdat alle openbare methoden, ** awesome_1 ** en ** awesome_2 **, overeenkomstige testmethoden hebben, * * test_awesome_1 ** en ** test_awesome_2 **. "-------------------------------------- -------------------------------- Ran 2-testen in 0,000s OK "Laten we een nieuwe methode toevoegen ** awesome_3 ** zonder een overeenkomstige test en voer de tests opnieuw uit. "python class AwesomeClass: def awesome_1 (self): return 'awesome!' def awesome_2 (zelf): retourneer 'geweldig! geweldig!' def awesome_3 (self): retourneer 'awesome! awesome! awesome! "Het opnieuw uitvoeren van de tests resulteert in de volgende uitvoer:" python3 a.py Traceback (laatste oproep laatste): Bestand "a.py", regel 25, in .