Écrivez vos propres décorateurs Python

Vue d'ensemble

Dans l'article intitulé Deep Dive Into Python Decorators, j'ai présenté le concept de décorateurs Python, présenté de nombreux décorateurs cool et expliqué comment les utiliser..

Dans ce tutoriel, je vais vous montrer comment écrire vos propres décorateurs. Comme vous le verrez, écrire vos propres décorateurs vous donne beaucoup de contrôle et vous permet de nombreuses fonctionnalités. Sans décorateurs, ces capacités nécessiteraient beaucoup de passe-partout répétitifs générateurs d'erreurs qui encombrent votre code ou des mécanismes complètement externes tels que la génération de code.

Une récapitulation rapide si vous ne connaissez rien aux décorateurs. Un décorateur est un appelable (fonction, méthode, classe ou objet avec un appel()) qui accepte un appelable en entrée et renvoie un appelable en sortie. En règle générale, l'appelable retourné fait quelque chose avant et / ou après avoir appelé l'entrée appelable. Vous appliquez le décorateur en utilisant le @ syntaxe. Beaucoup d'exemples à venir…

Le décorateur Hello World

Commençons par un «Bonjour tout le monde! décorateur. Ce décorateur remplacera totalement tout objet décoratif décoré par une fonction imprimant simplement "Hello World!".

python def hello_world (f): def décoré (* arguments, ** kwargs): imprimez 'Bonjour tout le monde!' retour décoré

C'est tout. Voyons-le en action, puis expliquons les différentes pièces et leur fonctionnement. Supposons que nous ayons la fonction suivante qui accepte deux numéros et imprime leur produit:

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

Si vous invoquez, vous obtenez ce que vous attendez:

multiplier (6, 7) 42

Disons le décorer avec notre Bonjour le monde décorateur en annotant la multiplier fonctionner avec @Bonjour le monde.

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

Maintenant, quand vous appelez multiplier avec tous les arguments (y compris les types de données incorrects ou le nombre d'arguments erroné), le résultat est toujours 'Hello World!' imprimé.

"python multiplie (6, 7) Bonjour tout le monde!

multiplier () Bonjour tout le monde!

multiplie ('zzz') Bonjour tout le monde! "

D'ACCORD. Comment ça marche? La fonction de multiplication d’origine a été complètement remplacée par la fonction décorée imbriquée dans la Bonjour le monde décorateur. Si nous analysons la structure de la Bonjour le monde décorateur alors vous verrez qu'il accepte l'entrée appelable F (qui n’est pas utilisé dans ce décorateur simple), il définit une fonction imbriquée appelée décoré qui accepte toute combinaison d'arguments et d'arguments de mots clés (def décoré (* args, ** kwargs)), et finalement il retourne le décoré une fonction.

Décorateurs de fonctions d'écriture et de méthodes

Il n'y a pas de différence entre écrire une fonction et un décorateur de méthode. La définition du décorateur sera la même. Le callable d’entrée sera soit une fonction régulière, soit une méthode liée.

Vérifions cela. Voici un décorateur qui imprime simplement l'appelable d'entrée et le type avant de l'invoquer. C’est très typique pour un décorateur d’effectuer une action et de continuer en invoquant le callable original..

python def print_callable (f): def decoré (* arguments, ** kwargs): print f, type (f) retourne f (* args, ** kwargs) retourne décoré

Notez la dernière ligne qui appelle l'entrée appelable de manière générique et renvoie le résultat. Ce décorateur est non intrusif en ce sens que vous pouvez décorer n’importe quelle fonction ou méthode dans une application opérationnelle. L’application continuera à fonctionner car la fonction décorée invoque l’original et a juste un effet secondaire peu avant..

Voyons cela en action. Je décorerai à la fois notre fonction de multiplication et une méthode.

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

classe A (objet): @print_callable def foo (auto): affiche 'foo () ici "

Lorsque nous appelons la fonction et la méthode, l'appelable est imprimé, puis ils effectuent leur tâche d'origine:

"multiplier les pythons (6, 7) 42

A (). Foo () foo () ici "

Décorateurs avec des arguments

Les décorateurs peuvent aussi prendre des arguments. Cette possibilité de configurer le fonctionnement d'un décorateur est très puissante et vous permet d'utiliser le même décorateur dans de nombreux contextes..

Supposons que votre code est trop rapide et que votre patron vous demande de le ralentir un peu, car vous faites en sorte que les autres membres de l'équipe aient l'air mauvais. Ecrivons un décorateur qui mesure la durée d'exécution d'une fonction et si elle s'exécute en moins d'un certain nombre de secondes t, il attendra que t secondes n'expire, puis revienne.

Ce qui est différent maintenant est que le décorateur lui-même prend un argument t qui détermine le temps d’exécution minimum, et différentes fonctions peuvent être décorées avec différentes durées d’exécution. En outre, vous remarquerez que lors de l'introduction d'arguments de décorateur, deux niveaux d'imbrication sont nécessaires:

"heure d'importation en python

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

Déballons-le. Le décorateur lui-même - la fonction minimum_runtime prend une dispute t, qui représente le temps d’exécution minimum du callable décoré. L'entrée appelable F a été «poussé» vers le niché décoré fonction, et les arguments appelables d’entrée ont été «poussés» vers une autre fonction imbriquée emballage.

La logique réelle se déroule à l'intérieur du emballage une fonction. L'heure de début est enregistrée, l'appelant d'origine F est invoqué avec ses arguments et le résultat est stocké. Ensuite, l'exécution est vérifiée, et si elle est inférieure au minimum t puis il dort pour le reste du temps puis revient.

Pour le tester, je vais créer quelques fonctions qui se multiplient et les décorer avec des délais différents..

"python @minimum_runtime (1) par slow_multiply (x, y): multiplie (x, y)

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

Maintenant, je vais appeler multiplier directement ainsi que les fonctions plus lentes et mesurer le temps.

"heure d'importation en python

funcs = [multiplier, slow_multiply, slower_multiply] pour f dans funcs: start = time.time () f (6, 7) print f, time.time () - start "

Voici la sortie:

42 plaine 1.59740447998e-05 42 1,00477004051 42 3.00489807129

Comme vous pouvez le constater, la multiplication initiale ne prend presque pas de temps et les versions plus lentes sont en effet retardées en fonction de la durée d’exécution minimum.

Un autre fait intéressant est que la fonction décorée exécutée est le wrapper, ce qui est logique si vous suivez la définition du décoré. Mais cela pourrait être un problème, surtout si nous avons affaire à des décorateurs de piles. La raison en est que de nombreux décorateurs inspectent également leurs entrées appelables et vérifient son nom, sa signature et ses arguments. Les sections suivantes examineront cette question et donneront des conseils sur les meilleures pratiques..

Décorateurs d'objets

Vous pouvez également utiliser des objets en tant que décorateurs ou renvoyer des objets à vos décorateurs. La seule exigence est qu’ils aient un __appel__() méthode, ils sont donc appelables. Voici un exemple de décorateur basé sur les objets qui compte le nombre de fois où sa fonction cible est appelée:

Classe python Compteur (objet): def __init __ (self, f): self.f = f self.called = 0 def __call __ (self, * arguments, ** kwargs): self.called + = 1 return self.f (* args, ** kwargs)

Ici c'est en action:

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

bbb () bbb

bbb () bbb

bbb () bbb

print bbb.called 3 "

Choisir entre les décorateurs basés sur les fonctions et les objets

C'est principalement une question de préférence personnelle. Les fonctions imbriquées et les fermetures de fonctions fournissent toute la gestion d'état proposée par les objets. Certaines personnes se sentent plus à l'aise avec des cours et des objets.

Dans la section suivante, je parlerai des décorateurs sages, et les décorateurs basés sur des objets prennent un peu de travail supplémentaire pour bien se comporter..

Décorateurs bien élevés

Les décorateurs à usage général peuvent souvent être empilés. Par exemple:

python @ decorator_1 @ decorator_2 def foo (): affiche 'foo () ici'

Lors de l'empilement de décorateurs, le décorateur extérieur (décorateur_1 dans ce cas) recevra le rappelable retourné par le décorateur intérieur (décorateur_2). Si decorator_1 dépend en quelque sorte du nom, des arguments ou de la chaîne de documentation de la fonction d'origine et que decorator_2 est implémenté naïvement, alors décorator_2 ne verra pas les informations correctes de la fonction d'origine, mais uniquement l'appelable renvoyé par decorator_2..

Par exemple, voici un décorateur qui vérifie que le nom de sa fonction cible est tout en minuscule:

python def check_lowercase (f): def decoré (* arguments, ** kwargs): assert f.func_name == f.func_name.lower () f (* arguments, ** kwargs) renvoie décoré

Décorez une fonction avec elle:

python @check_lowercase def Foo (): affiche 'Foo () ici'

L'appel de Foo () donne lieu à une assertion:

"plain In [51]: Foo () - Traceback AssertionErreur (l'appel le plus récent en dernier)

dans () ----> 1 Foo () en décoré (* args, ** kwargs) 1 def check_lowercase (f): 2 def décoré (* args, ** kwargs): ----> 3 assert f.func_name == f.func_name.lower () 4 return Decorated "Mais si nous empilons le décorateur ** check_lowercase ** sur un décorateur tel que ** hello_world ** qui renvoie une fonction imbriquée appelée 'ornées', le résultat est très différent:" python @check_lowercase @hello_world def Foo (): print ' Foo () ici 'Foo () Hello World! "Le décorateur ** check_lowercase ** n'a pas émis d'affirmation car il ne voyait pas le nom de la fonction' Foo '. Il s'agit d'un problème grave. Le comportement approprié pour un décorateur est de préserver autant que possible les attributs de la fonction d'origine. Voyons comment procéder. Je vais maintenant créer un décorateur de shell qui appelle simplement son entrée appelable, mais conserve toutes les informations de la fonction d'entrée: le nom de la fonction, tous ses attributs (au cas où un décorateur interne ajouterait des attributs personnalisés) et sa docstring. "python def passthrough (f): def décoré (* args, ** kwargs): f (* args , ** kwargs) décoré .__ nom__ = f .__ nom__ décoré .__ nom__ = f .__ module__ décoré .__ dict__ = f .__ dict__ décoré .__ doc__ = f .__ doc__ retour décoré "Maintenant, décorateurs empilés sur le décorateur ** passe-partout ** fonctionnera comme si elles décoraient directement la fonction cible. "python @check_lowercase @passthrough def Foo (): affiche 'Foo () ici" ### Utilisation du @wraps Decorator Cette fonctionnalité est si utile que la bibliothèque standard a une décorateur dans le module functools appelé ['wraps'] (https://docs.python.org/2/library/functools.html#functools.wraps) pour aider à écrire des décorateurs appropriés qui fonctionnent bien avec d'autres décorateurs. Vous décorez simplement la fonction renvoyée dans votre décorateur avec ** @ wraps (f) **. Voyez à quel point ** passthrough ** est plus concis en utilisant ** wraps **: "python importé de functools wraps def passthrough (f): @wraps (f) def décoré (* args, ** kwargs): f (* args, ** kwargs) return decored "Je recommande vivement de toujours l'utiliser sauf si votre décorateur est conçu pour modifier certains de ces attributs. ## Écrire aux décorateurs de classe Les décorateurs de classe ont été introduits dans Python 3.0. Ils opèrent sur une classe entière. Un décorateur de classe est appelé lorsqu'une classe est définie et avant que des instances ne soient créées. Cela permet au décorateur de classe de modifier pratiquement tous les aspects de la classe. En général, vous allez ajouter ou décorer plusieurs méthodes. Passons directement à un exemple sophistiqué: supposons que vous ayez une classe appelée 'AwesomeClass' avec un tas de méthodes publiques (méthodes dont le nom ne commence pas par un trait de soulignement comme __init__) et que vous avez une classe de test basée sur unittests appelée 'AwesomeClassTest '. AwesomeClass n'est pas seulement génial, mais aussi très critique, et vous voulez vous assurer que si quelqu'un ajoute une nouvelle méthode à AwesomeClass, il ajoute également une méthode de test correspondante à AwesomeClassTest. Voici la AwesomeClass: "classe python AwesomeClass: def awesome_1 (self): return 'awesome!' def awesome_2 (self): return 'awesome! awesome! "Voici AwesomeClassTest:" python importé unittest TestCase, classe principale AwesomeClassTest (TestCase): def test_awesome_1 (self): r = AwesomeClass (). awesome_1 (awesome_1 (auto) def test_awesome_2 (self): r = AwesomeClass (). awesome_2 () self.assertEqual ('awesome! awesome!', r) si __name__ == '__main__': main () "Maintenant, si quelqu'un ajoute une méthode ** awesome_3 ** avec un bogue, les tests seront quand même réussis car il n'y a pas de test qui appelle ** awesome_3 **. Comment pouvez-vous garantir qu'il existe toujours une méthode de test pour chaque méthode publique? Bien, vous écrivez un décorateur de classe, bien sûr. Le décorateur de la classe @ensure_tests décellera l'AwesomeClassTest et s'assurera que chaque méthode publique a une méthode de test correspondante. "Python def 148 )] public_methods = [k pour k, v dans target_class .__ dict __. items () si callable (v) et non k.startswith ('_')] # Préfixe 'test_' à partir des noms de méthode de test test_methods = [m [5 :] for m in test_methods] if set (test_methods)! = set (public_methods): raise RuntimeError ('Test / méthodes publiques incompatibles!') return cls "Cela semble très bien, mais il y a un problème. Les décorateurs de classe acceptent un seul argument: la classe décorée. Le décorateur Ensure_tests a besoin de deux arguments: la classe et la classe cible. Je ne pouvais pas trouver un moyen d'avoir des décorateurs de classe avec des arguments similaires à ceux des décorateurs de fonction. N'ai pas peur. Python a la fonction [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) juste pour ces cas. "Python @partial (sure_tests, target_class = AwesomeClass) awesomeClassTest (TestCase): def test_awesome_1 (auto): r = AwesomeClass (). Awesome_1 () self.assertEqual ('awesome!', R) def test_awesome_2 (auto): r = AwesomeClass (). Awesome_2 () self.assertEqual (') awesome! awesome! ', r) si __name__ ==' __main__ ': main () "L'exécution des tests aboutit à un succès, car toutes les méthodes publiques, ** awesome_1 ** et ** awesome_2 **, ont des méthodes de test correspondantes, * * test_awesome_1 ** et ** test_awesome_2 **. "------------------------------------------ -------------------------------- Ran 2 tests in 0.000s OK "Ajoutons une nouvelle méthode ** awesome_3 ** sans test correspondant et relancez les tests. "classe python AwesomeClass: def awesome_1 (auto): renvoie 'awesome!' def awesome_2 (self): return 'génial! génial!' def awesome_3 (self): return 'awesome! awesome! awesome! "L'exécution des tests à nouveau donne le résultat suivant:" python3 a.py Traceback (l'appel le plus récent en dernier): Fichier "a.py", ligne 25, dans .