Schreiben Sie Ihre eigenen Python-Dekorateure

Überblick

In dem Artikel Deep Dive Into in Python Decorators habe ich das Konzept der Python-Dekorateure vorgestellt, viele coole Dekorateure gezeigt und deren Verwendung erklärt.

In diesem Tutorial zeige ich Ihnen, wie Sie Ihre eigenen Dekorateure schreiben. Wenn Sie Ihre eigenen Dekorateure schreiben, haben Sie viel Kontrolle und viele Möglichkeiten. Ohne Dekorateure würden diese Funktionen viele fehleranfällige und sich wiederholende Boilerplates erfordern, die Ihren Code oder völlig externe Mechanismen wie die Codegenerierung durcheinanderbringen.

Eine kurze Zusammenfassung, wenn Sie nichts über Dekorateure wissen. Ein Dekorator ist eine aufrufbare Funktion (Funktion, Methode, Klasse oder Objekt mit einem Anruf() -Methode), die ein Aufrufbares als Eingabe akzeptiert und ein Aufrufbares als Ausgabe zurückgibt. Normalerweise führt der zurückgegebene aufrufbare Befehl vor und / oder nach dem Aufruf der aufrufbaren Eingabe etwas aus. Sie wenden den Dekorateur mit dem @ an. Syntax. Viele Beispiele folgen in Kürze…

Der Hello World Decorator

Beginnen wir mit einer "Hallo Welt!" Dekorateur. Dieser Dekorateur ersetzt jeden dekorierten Callable vollständig durch eine Funktion, die nur "Hello World!".

python def hello_world (f): def verziert (* args, ** kwargs): print 'Hello World!' verziert zurückkehren

Das ist es. Lassen Sie uns es in Aktion sehen und dann die verschiedenen Teile erklären und wie es funktioniert. Angenommen, wir haben die folgende Funktion, die zwei Zahlen akzeptiert und ihr Produkt druckt:

python def multiplizieren (x, y): x * y drucken

Wenn Sie aufrufen, erhalten Sie, was Sie erwarten:

multiplizieren (6, 7) 42

Lass es uns mit unserem schmücken Hallo Welt Dekorateur durch Kommentieren der multiplizieren Funktion mit @Hallo Welt.

python @hello_world def multiplizieren (x, y): x * y drucken

Nun, wenn Sie anrufen multiplizieren Bei allen Argumenten (einschließlich falscher Datentypen oder falscher Anzahl von Argumenten) lautet das Ergebnis immer "Hello World!" gedruckt.

"Python multiplizieren (6, 7) Hello World!

multiplizieren () Hallo Welt!

multiplizieren Sie ('zzz') Hallo Welt! "

OK. Wie funktioniert es? Die ursprüngliche Multiplikationsfunktion wurde vollständig durch die verschachtelte Dekorationsfunktion im ersetzt Hallo Welt Dekorateur. Wenn wir die Struktur der analysieren Hallo Welt Dekorateur dann werden Sie sehen, dass er die aufrufbare Eingabe akzeptiert f (was in diesem einfachen Dekorateur nicht verwendet wird), definiert eine verschachtelte Funktion, die aufgerufen wird verziert Das akzeptiert jede Kombination von Argumenten und Schlüsselwortargumenten (def verziert (* args, ** kwargs)), und schließlich wird das zurückgegeben verziert Funktion.

Schreibfunktion und Method Decorators

Es gibt keinen Unterschied zwischen dem Schreiben einer Funktion und einem Methodendekorateur. Die Dekoratordefinition wird dieselbe sein. Die aufrufbare Eingabe ist entweder eine reguläre Funktion oder eine gebundene Methode.

Lassen Sie uns das überprüfen. Hier ist ein Dekorateur, der die aufrufbare Eingabe und den Typ vor dem Aufruf druckt. Dies ist sehr typisch für einen Dekorateur, der eine Aktion ausführt und mit dem Aufruf des ursprünglichen Aufrufbaren fortfährt.

python def print_callable (f): def Dekor (* args, ** kwargs): print f, Typ (f) return f (* args, ** kwargs) return dekoriert

Beachten Sie die letzte Zeile, die die aufrufbare Eingabe auf generische Weise aufruft und das Ergebnis zurückgibt. Dieser Dekorateur ist nicht aufdringlich in dem Sinne, dass Sie jede Funktion oder Methode in einer funktionierenden Anwendung dekorieren können. Die Anwendung funktioniert weiterhin, da die dekorierte Funktion das Original aufruft und nur einen kleinen Nebeneffekt aufweist.

Lass es uns in Aktion sehen. Ich werde sowohl unsere Multiplikationsfunktion als auch eine Methode dekorieren.

"python @print_callable def multiplizieren (x, y): x * y drucken

Klasse A (Objekt): @print_callable def foo (self): "foo () hier drucken"

Wenn wir die Funktion und die Methode aufrufen, wird das Callable gedruckt und dann führen sie ihre ursprüngliche Aufgabe aus:

"Python-Multiplikation (6, 7) 42

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

Dekorateure mit Argumenten

Dekorateure können auch Argumente annehmen. Diese Möglichkeit, den Betrieb eines Dekorators zu konfigurieren, ist sehr leistungsfähig und ermöglicht es Ihnen, denselben Dekorator in vielen Zusammenhängen zu verwenden.

Angenommen, Ihr Code ist viel zu schnell und Ihr Chef bittet Sie, den Code etwas zu verlangsamen, weil Sie die anderen Teammitglieder schlecht aussehen lassen. Lassen Sie uns einen Dekorator schreiben, der misst, wie lange eine Funktion läuft und ob sie in weniger als einer bestimmten Anzahl von Sekunden ausgeführt wird t, es wird warten, bis t Sekunden abgelaufen sind, und dann zurückkehren.

Was jetzt anders ist, ist, dass der Dekorateur selbst ein Argument führt t das bestimmt die minimale Laufzeit, und verschiedene Funktionen können mit unterschiedlichen minimalen Laufzeiten versehen werden. Sie werden auch feststellen, dass bei der Einführung von Decorator-Argumenten zwei Schachtelungsebenen erforderlich sind:

"Pythonimportzeit

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

Lass es uns auspacken. Der Dekorateur selbst - die Funktion Mindestlaufzeit nimmt ein Argument an t, was die minimale Laufzeit für das verzierte Callable darstellt. Die Eingabe kann aufgerufen werden f wurde auf die verschachtelten "heruntergedrückt" verziert Funktion und die aufrufbaren Argumente der Eingabe wurden zu einer weiteren verschachtelten Funktion "verschoben" Verpackung.

Die eigentliche Logik findet innerhalb der statt Verpackung Funktion. Die Startzeit wird aufgezeichnet, das Original kann abgerufen werden f wird mit seinen Argumenten aufgerufen und das Ergebnis wird gespeichert. Dann wird die Laufzeit geprüft und ob sie unter dem Minimum liegt t dann schläft es für den Rest der Zeit und kehrt dann zurück.

Um es zu testen, erstelle ich ein paar Funktionen, die mehrfach aufgerufen und mit unterschiedlichen Verzögerungen verziert werden.

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

@minimum_runtime (3) def langsamer_multiply (x, y): multiplizieren (x, y) "

Ich werde jetzt anrufen multiplizieren direkt sowie die langsameren Funktionen und messen die Zeit.

"Pythonimportzeit

funcs = [multiplizieren, langsam_multiply, langsamer_multiply] für f in funcs: start = time.time () f (6, 7) print f, time.time () - start "

Hier ist die Ausgabe:

plain 42 1.59740447998d-05 42 1.00477004051 42 3.00489807129

Wie Sie sehen, hat das ursprüngliche Multiplizieren fast keine Zeit in Anspruch genommen, und die langsameren Versionen waren tatsächlich entsprechend der angegebenen Mindestlaufzeit verzögert.

Eine andere interessante Tatsache ist, dass die ausgeführte dekorierte Funktion der Wrapper ist, was sinnvoll ist, wenn Sie der Definition des dekorierten folgen. Aber das könnte ein Problem sein, besonders wenn es um Stapeldekorateure geht. Der Grund ist, dass viele Dekorateure auch ihre aufrufbaren Eingaben überprüfen und ihren Namen, ihre Signatur und ihre Argumente überprüfen. In den folgenden Abschnitten wird dieses Problem behandelt und Empfehlungen für bewährte Verfahren gegeben.

Objektdekorateure

Sie können Objekte auch als Dekorateure verwenden oder Objekte von Ihren Dekorateuren zurückgeben. Die einzige Voraussetzung ist, dass sie eine haben __Anruf__() Methode, also sind sie aufrufbar. Hier ein Beispiel für einen objektbasierten Dekorateur, der zählt, wie oft seine Zielfunktion aufgerufen wird:

Python-Klasse Counter (Objekt): def __init __ (self, f): self.f = f self.called = 0 def __call__ (self, * args, ** kwargs): self.called + = 1 return self.f (* args, ** kwargs)

Hier ist es in Aktion:

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

bbb () bbb

bbb () bbb

bbb () bbb

print bbb.called 3 "

Auswahl zwischen funktionsbasierten und objektbasierten Dekorateuren

Dies ist meistens eine Frage der persönlichen Vorlieben. Verschachtelte Funktionen und Funktionsschließungen bieten die gesamte Zustandsverwaltung, die Objekte bieten. Manche Menschen fühlen sich mit Klassen und Objekten wohler.

Im nächsten Abschnitt werde ich auf gut erzogene Dekorateure eingehen, und objektbasierte Dekorateure benötigen etwas mehr Arbeit, um gut benommen zu sein.

Wohlerzogene Dekorateure

Mehrzweck-Dekorateure können oft gestapelt werden. Zum Beispiel:

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

Beim Stapeln von Dekorateuren erhält der Außendekorateur (in diesem Fall Dekorateur_1) die Callable, die vom Innendekorateur (Decorator_2) zurückgegeben wird. Wenn decorator_1 in gewisser Weise von Name, Argumenten oder docstring der ursprünglichen Funktion abhängt und decorator_2 naiv implementiert wird, werden bei decorator_2 nicht die korrekten Informationen der ursprünglichen Funktion angezeigt, sondern nur die von decorator_2 zurückgegebene aufrufbare Funktion.

Hier ist zum Beispiel ein Dekorateur, der überprüft, ob der Name der Zielfunktion nur aus Kleinbuchstaben besteht:

python def check_lowercase (f): def decor (* args, ** kwargs): f.func_name == f.func_name.lower () f (* args, ** kwargs) wird mit dekoriert zurückgegeben

Lassen Sie uns damit eine Funktion dekorieren:

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

Das Aufrufen von Foo () führt zu einer Assertion:

"plain In [51]: Foo () - AssertionError Traceback (letzter Aufruf zuletzt)

im () ----> 1 Foo () in decor (* args, ** kwargs) 1 def check_lowercase (f): 2 def decor (* args, ** kwargs): ----> 3 assert f.func_name == f.func_name.lower () 4 return decor "Wenn wir jedoch den ** check_lowercase ** -Dekorateur über einen Decorator wie ** hello_world ** stapeln, der eine verschachtelte Funktion namens 'decor' zurückgibt, ist das Ergebnis ganz anders:" python @check_lowercase @hello_world def Foo (): print ' Foo () hier 'Foo () Hallo Welt! "Der ** check_lowercase ** -Dekorateur hat keine Behauptung erhoben, weil er den Funktionsnamen' Foo 'nicht gesehen hat. Dies ist ein ernstes Problem. Das richtige Verhalten für einen Dekorateur soll so viele Attribute der ursprünglichen Funktion wie möglich bewahren. Sehen wir uns an, wie es gemacht wird. Ich werde jetzt einen Shell-Dekorator erstellen, der die Eingabe einfach aufrufbar aufruft, aber alle Informationen der Eingabefunktion beibehält: den Funktionsnamen. alle seine Attribute (falls ein innerer Dekorateur einige benutzerdefinierte Attribute hinzugefügt hat) und seinen docstring. "python def passthrough (f): def decor (* args, ** kwargs): f (* args , ** kwargs) verziert .__ name__ = f .__ name__ verziert .__ name__ = f .__ modul__ verziert .__ dict__ = f .__ dict__ verziert .__ doc__ = f .__ doc__ return decor "Jetzt sind Dekorateure auf dem ** Passthrough ** -Dekorateur ** gestapelt funktioniert genauso, als würden sie die Zielfunktion direkt dekorieren. "python @check_lowercase @passthrough def Foo (): print 'Foo () here" ### Verwenden des @ wraps Decorators Diese Funktion ist so nützlich, dass die Standardbibliothek eine spezielle Funktion hat Dekorateur im functools-Modul ['wraps'] (https://docs.python.org/2/library/functools.html#functools.wraps), um richtige Dekoratoren zu erstellen, die gut mit anderen Dekorateuren zusammenarbeiten. Sie schmücken die zurückgegebene Funktion einfach mit ** @ wraps (f) ** in Ihrem Dekorateur. Sehen Sie, wie viel prägnanter ** Passthrough ** bei Verwendung von ** wraps ** aussieht: "Python von functools import wraps def passthrough (f): @ wraps (f) def decor (* args, ** kwargs): f (* args, ** kwargs) return decor "Ich empfehle dringend, es immer zu verwenden, es sei denn, Ihr Dekorateur kann einige dieser Attribute ändern. ## Writing Class Decorators Klassendekoratoren wurden in Python 3.0 eingeführt. Sie operieren für eine ganze Klasse. Ein Klassendekorator wird aufgerufen, wenn eine Klasse definiert wird und bevor Instanzen erstellt werden. Dadurch kann der Klassendekorateur so ziemlich jeden Aspekt der Klasse ändern. In der Regel fügen Sie mehrere Methoden hinzu oder verzieren sie. Lassen Sie uns gleich zu einem schillernden Beispiel springen: Nehmen wir an, Sie haben eine Klasse namens 'AwesomeClass' mit einer Reihe öffentlicher Methoden (Methoden, deren Name nicht mit einem Unterstrich wie __init__ beginnt) und Sie haben eine Test-basierte Klasse, die 'AwesomeClassTest' heißt '. AwesomeClass ist nicht nur großartig, sondern auch sehr kritisch. Sie möchten sicherstellen, dass jemand, der AwesomeClass eine neue Methode hinzufügt, auch eine entsprechende Testmethode zu AwesomeClassTest hinzufügt. Hier ist die AwesomeClass: "Python-Klasse AwesomeClass: def awesome_1 (self): return 'awesome!" def awesome_2 (self): return 'awesome! awesome! "Hier ist der AwesomeClassTest:" Python aus unittest import TestCase, Hauptklasse 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 ('awesome! awesome!', r) if __name__ == '__main__': main () "Nun, Wenn jemand eine ** awesome_3 ** -Methode mit einem Fehler hinzufügt, werden die Tests trotzdem bestanden, da es keinen Test gibt, der ** awesome_3 ** aufruft. Wie können Sie sicherstellen, dass es für jede öffentliche Methode immer eine Testmethode gibt? Nun, Sie schreiben natürlich einen Klassendekorateur. Der Dekorateur der Klasse @ensure_tests schmückt den AwesomeClassTest und stellt sicher, dass jede öffentliche Methode über eine entsprechende Testmethode verfügt. "Python def sichert_tests (cls, target_class): test_methods = [m für m in cls .__ dict__ if m.startswith ('test_' )] public_methods = [k für k, v in target_class .__ dict __. items () wenn aufrufbar (v) und nicht k.startswith ('_')] # Strip 'test_' Präfix von Testmethodennamen test_methods = [m [5] :] für m in test_methods] if set (test_methods)! = set (public_methods): RuntimeError erhöhen ('Test / public Methods mismatch!') return cls "Das sieht ziemlich gut aus, aber es gibt ein Problem. Klassendekorateure akzeptieren nur ein Argument: die dekorierte Klasse. Der Dekorator "sure_tests" benötigt zwei Argumente: die Klasse und die Zielklasse. Ich habe keine Möglichkeit gefunden, Klassendekorateure mit Argumenten zu haben, die Funktionsdekoratoren ähneln. Hab keine Angst. Python hat die Funktion [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) nur für diese Fälle. "Python @partial (sure_tests, target_class = AwesomeClass) - Klasse AwesomeClassTest (Testfall): def test_awesome_1 (self): r = AwesomeClass (). Awesome_1 () self.assertEqual ('awesome!', R) def test_awesome_2 (self): r = AwesomeClass (). Awesome_2 () self.assertEqual (' awesome! awesome! ', r) if __name__ ==' __main__ ': main () "Das Ausführen der Tests führt zum Erfolg, da alle öffentlichen Methoden ** awesome_1 ** und ** awesome_2 ** über entsprechende Testmethoden verfügen. * * test_awesome_1 ** und ** test_awesome_2 **. "---------------------------------- -------------------------------- Lief 2 Tests in 0,000s OK "Fügen wir eine neue Methode hinzu ** awesome_3 ** ohne einen entsprechenden Test und führe die Tests erneut aus. "python class AwesomeClass: def awesome_1 (self): Gib 'awesome!' def awesome_2 (self): Gib 'awesome! awesome!' zurück. def awesome_3 (self): return 'awesome! awesome! awesome! "Beim erneuten Ausführen der Tests wird folgende Ausgabe ausgegeben:" python3 a.py Traceback (letzter Aufruf zuletzt): Datei "a.py", Zeile 25, in .