Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Python

Composez vos décorateurs en Python

by
Difficulty:IntermediateLength:MediumLanguages:

French (Français) translation by Stéphane Esteve (you can also view the original English article)

Tour d'horizon

Dans l'article Deep Dive Into Python Decorators, J'avais présenté le concept des décorateurs Python, en montrant leur élégance et en expliquant comment les utiliser.

Au cours de ce tutoriel, je vais vous apprendre à écrire vos propres décorateurs. Comme vous le constaterez, rédiger ses décorateurs offre une large gamme de contrôles et de possibilités. Sans décorateurs, ces éventualités nécessiteraient tout un tas d'erreurs et de motifs à répétition, qui encombreraient votre code ou feraient appel à des mécanismes externes, comme la génération de code.

Parcourons ensemble un récapitulatif des décorateurs, si vous ne les connaissez pas. Un décorateur est un "callable" (fonction, méthode, classe ou objet avec une méthode call()) qui reçoit en entrée une fonction auto-appelée et renvoie en sortie une autre fonction auto-appelée. Typiquement, la fonction auto-appelée applique d'abord quelque chose avant et/ou après avoir exécuté l'autre fonction auto-appelée. Pour appliquer ce décorateur, on insère préalablement le signe @ . De nombreux exemples bientôt...

Le décorateur "Hello World"

Commençons avec un simple décorateur "Hello World". Ce décorateur remplacera complètement tout appel décoré par une fonction qui affichera systématiquement "Hello World!".

C'est tout. Observons-le en action et expliquons chaque éléments pour comprendre son fonctionnement. Supposons que nous avons la fonction suivante qui reçoit deux nombres et affiche leur produit :

En l'invoquant, vous obtenez le résultat attendu :

Tentons de la décorer avec notre hello_world en annotant la fonction multiply avec @hello_world.

Ainsi, dès que vous exécutez multiply avec tous ses arguments (même avec un typage d'arguments erroné ou une quantité d'arguments fausse), il s'affichera systématiquement "Hello World!".

OK. Comment ça marche ? La fonction originelle multiply a totalement été remplacée par la fonction décorée et encapsulée contenu par hello_world. Si nous analysons la structure du décorateur hello_world, constatez qu'il accueille l'entrée auto-appelée f (qui n'est pas pas exploité par ce simple décorateur). Il définit une fonction encapsulée identifiée comme décorée, recevant toute combinaison d'arguments et de mots-clés (def decorated(*args, **kwargs) ). Enfin, il renvoie le résultat de la fonction décorée.

Composer une fonction et une méthode décorées

Il n'y a pas de différence notable entre écrire une fonction et une méthode décorée. La définition du décorateur restera la même. L'entrée auto-appelée sera aussi bien une fonction régulière ou une méthode associée.

Vérifions l'ensemble. Créons ici un décorateur qui affichera l'entrée auto-appelée et son type avant de l'invoquer. C'est le comportement habituel pour un décorateur d'exécuter une certaine action et accomplie le "callable" original.

Observez la dernière ligne qui fait appel à l'entrée auto-appelée d'une façon générale et renvoie le résultat. Le décorateur est non-intrusif dans la mesure où vous pouvez décorer n'importe quelle fonction ou méthode dans une application qui fonctionne. L'application ne peut pas planter parce que la fonction décorée appelle toujours l'originale et lui ajoute préalablement un léger effet de bord.

Voyons ça en action. Je vais décorer ensemble notre fonction multiply et une méthode.

Dès que vous invoquez la fonction et la méthode, le "callable" est affiché et ensuite, ils exécutent leur tâche native :

Des décorateurs avec arguments

Les décorateurs peuvent aussi comporter des arguments. Cette manière de configurer son action est très puissante et vous permet d'utiliser le même décorateur dans des contextes variés.

Imaginons que votre code est trop rapide à s'exécuter, et que votre patron vous demande de le ralentir un petit peu parce qu'il rend dingue l'autre équipe de développement. Composons un décorateur qui mesure le temps d'exécution total de votre fonction, et si elle tourne plus vite qu'un certain nombre de secondes t, il freinera cette exécution jusqu'à ce que les t secondes expirent et renverra le résultat.

La différence ici est que le décorateur prend un argument t qui détermine le temps d'exécution minimum, et différentes fonctions peuvent désormais être décorées avec des temps différents. Par ailleurs, vous constaterez qu'en ajoutant des arguments au décorateur, deux niveaux d'encapsulation sont nécessaires :

Démontons tout ça. Le décorateur lui-même - la fonction minimum_runtime prend un argument t, qui définit le temps minimum d'exécution pour la fonction décorée. La fonction "callable" f est repoussée vers la fonction décorée et encapsulée. Et ses arguments sont aussi renvoyés vers la fonction contenue juste après appelée wrapper.

Toute la logique du décorateur prend ainsi toute sa dimension au sein de la fonction wrapper. Le temps de départ est mémorisé, le "callable" original f est appelé avec ses arguments, et le résultat est conservé. Enfin, le temps d'exécution est validé, et s'il est inférieur au minimum t alors il bloque le script selon cet argument et renvoie le résultat.

Pour le tester, j'ai créé un bloc de fonctions qui fait appel à mulitply et les décore avec différents temps d'attente.

Dès lors, j'appelle la fonction multiply directement ainsi que les fonctions lentes (slower) et évalue leur temps respectif.

Voici leur résultat :

Comme vous pouvez le voir, la fonction originale ne prends quasiment pas de temps, et ses versions lentes ont été retardées selon le temps minimum déclaré.

Autre fait intéressant : toute la logique est contenue dans le wrapper, ce qui est pertinent, si l'on suit la définition du décorateur. Mais ce mécanisme peut vite devenir problématique, surtout si nous devons empiler plusieurs décorateurs. La raison à cette limite provient du fait que plusieurs décorateurs observent leur entrée "callable" et vérifient leur nom, leur signature et leurs arguments. Les sections suivantes vont explorer cette problématique et fournir un certain nombre de conseils pour de meilleures pratiques.

Décorateurs d'objet

Vous pouvez aussi utiliser des objets comme décorateurs ou restituer des objets par vos décorateurs. La seule recommandation consiste à la nécessaire définition de la méthode __call__(), pour leur donner cette faculté. Vous trouverez ci-dessous un exemple pour un décorateur axé objet qui évalue le nombre de fois où sa fonction-cible est invoquée :

Et voici son exécution :

Comment choisir entre un décorateur axé fonction et un décorateur axé objet ?

Il s'agit ici davantage d'une question de choix personnel. Les fonctions encapsulées et autres délivrent toutes la même gestion d'état qu'un objet peut produire Certaines personnes se sentent plus à l'aise avec des classes et des objets.

Dans la partie qui va suivre, je commenterai les décorateurs" bienveillants", alors que les décorateurs axé objet demandent plus d'effort pour s'assurer de leur docilité.

Des décorateurs dociles

Les décorateurs d'usage courant peuvent très souvent être empilés. Par exemple :

Dans cet empilage de décorateurs, celui qui se situe à l'extérieur (decorator_1 dans ce cas) reçoit le "callable" renvoyé par celui qui se trouve à l'intérieur (decorator_2). Si le decorator_1 dépend d'une certaine manière du nom, des arguments ou des docstrings de la fonction originale, et que le decorator_2 est implémenté de façon naïve, alors le decorator_1 ne recevra que les informations renvoyées par le decorator_2, qui ne seront pas forcément conformes à ses attentes.

Par exemple, voici un décorateur qui observe si le nom de la fonction-cible est bien en minuscule :

Décorons-le d'une fonction :

Demandons les résultats de Foo() comme assertion :

Mais si nous empilons le décorateur check_lowercase par-dessus un décorateur comme hello_world qui produit une fonction encapsulée nommée 'décorée', le résultat est bien différent :

Le décorateur check_lowercase ne soulève plus d'assertion puisqu'il a perdu de vue le nom de la fonction 'Foo'. C'est ici que se trouve un réel dysfonctionnement. Le comportement attendu d'un décorateur est de préserver du mieux possible la plupart des attributs de la fonction originale.

Voyons comme ça se produit. Je vais désormais créer un décorateur-coquille qui récupère ses entrées "callable" tout en conservant toutes les informations de la fonction entrante : son nom, tous ses attributs (au cas où un décorateur précédent ajouterait d'autres attributs particuliers) et ses docstring.

Désormais, tous décorateurs situés au-dessus de ce décorateur passthrough devraient fonctionner comme s'ils enrobaient la fonction-cible directement.

Utilisation du décorateur @wraps

Cette fonctionnalité est si pratique que la librairie standard de Python a un décorateur spécial contenu dans le module functools et nommé 'wraps' afin de permettre la création de décorateurs qui fonctionnent parfaitement avec d'autres. Vous enrobez au sein de votre décorateur la fonction à renvoyer avec @wraps(f). Constatez avec quelle concision la fonction passthrough apparaît dès la déclaration avec le décorateur wraps :

Je recommande hautement son usage systématique à moins que votre décorateur est conçu pour modifier quelques-uns des attributs.

Construire des décorateurs avec des classes

Les décorateurs par classe ont été ajoutés dès Python 3.0. Ils fonctionne sur toute une classe. Un décorateur de classe est invoqué lorsqu'une classe est définie et avant que la moindre instance soit créée. Par ce biais, le décorateur de classe peut modifier à peu près tous les aspects de la classe sur laquelle il agit. En fait, vous pourrez ajouter ou décorer plusieurs méthodes.

Plongeons directement dans un bel exemple : supposons que vous ayez une classe appelée 'AwesomeClass' avec tout un tas de méthodes publiques (des méthodes dont leur nom ne commence pas par un soulignement, comme init) et que vous avez une classe basée sur des tests unitaires, appelée 'AwesomeClassTest'. AwesomeClass n'est pas seulement géniale, mais peut aussi être très dangereuse, et vous souhaitez vous assurer que si quelqu'un ajoute une nouvelle méthode à cette dernière, il ajoute aussi la méthode de test correspondante à AwesomeClassTest. Voici la classe AwesomeClass :

Et la classe AwesomeClassTest :

Aussi, si quelqu'un ajoute une méthode awesome_3 avec une erreur, les tests devraient toujours s'exécuter parce qu'il n'y a toujours pas l'appel vers awesome_3.

Comment être sûr qu'il y a toujours une méthode de test déclarée pour chaque méthode publique ? Bien entendu, rédigez un décorateur de classe. Le décorateur de classe @ensure_tests devra décorer la classe AwesomeClassTest et garantira que chaque méthode publique à sa propre méthode de test.

Tout paraît optimal, mais il y a un léger problème. Les décorateur de classe acceptent juste un seul argument : la classe décorée. Or, le décorateur ensure_test nécessite deux arguments : la classe et la classe-cible. Je n'ai pas réussi à trouver de façon de produire des décorateurs de classe avec des arguments comme pour les décorateurs de fonctions. N'ayez crainte. Python possède la fonction functools.partial conçue pour ce genre de cas.

Tout fonctionne parfaitement parce que pour chaque méthode publique awesome_1 et awesome_2 se trouvent les méthodes de test correspondantes comme test_awesome_1 et test_awesome_2.

Introduisons une nouvelle méthode awesome_3 sans sa méthode de test liée et exécutons les tests une nouvelle fois.

nous obtenons les résultats suivants en exécutant à nouveau les tests :

Le décorateur de classe révèle l'incohérence et vous le signifie haut et fort.

Conclusion

Composer des décorateurs Python est extraordinairement jouissif et vous permet d'enrober des tonnes de fonctionnalités, de manière ré-utilisable. Afin d'en tirer avantage et de les combiner de façon originale, vous devez prendre en compte les meilleures pratiques et leurs styles variés. Les décorateurs de classe dans Python 3 débloque une toute nouvelle dimension en personnalisant le comportement des classes.

Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.