Plongez dans les décorateurs en python

Vue d'ensemble

Les décorateurs Python sont l’une de mes fonctionnalités préférées de Python. Il s'agit de la mise en œuvre la plus conviviale * et * la plus conviviale pour les développeurs de la programmation par aspects que j'ai vue dans n'importe quel langage de programmation.. 

Un décorateur vous permet d’augmenter, de modifier ou de remplacer complètement la logique d’une fonction ou d’une méthode. Cette description sèche ne rend pas justice aux décorateurs. Une fois que vous avez commencé à les utiliser, vous découvrirez tout un univers d’applications soignées qui vous aideront à garder votre code précis et propre et à déplacer d’importantes tâches «administratives» hors du flux principal de votre code et dans un décorateur.. 

Avant de passer à quelques exemples intéressants, si vous souhaitez explorer un peu plus l’origine des décorateurs, les décorateurs de fonctions apparaissent tout d’abord dans Python 2.4. Voir PEP-0318 pour une discussion intéressante sur l'histoire, la raison d'être et le choix du nom de "décorateur". Les décorateurs de classe sont apparus en premier dans Python 3.0. Voir PEP-3129, qui est assez court et complète tous les concepts et idées des décorateurs de fonctions.

Exemples de décorateurs sympas

Il y a tellement d'exemples que j'ai du mal à choisir. Mon but ici est d'ouvrir votre esprit aux possibilités et de vous présenter des fonctionnalités super utiles que vous pouvez ajouter à votre code immédiatement en annotant littéralement vos fonctions avec une ligne..

Les exemples classiques sont les décorateurs @staticmethod et @classmethod intégrés. Ces décorateurs transforment une méthode de classe en une méthode statique (aucun premier argument self n'est fourni) ou en une méthode de classe (le premier argument est la classe et non l'instance).

Les décorateurs classiques

classe A (objet): @classmethod def foo (cls): print cls .__ name__ @staticmethod def bar (): print 'Je n'ai aucune utilité pour l'instance ou la classe' A.foo () A.bar () 

Sortie:

A je n'ai aucune utilité pour l'instance ou la classe

Les méthodes statiques et de classe sont utiles lorsque vous n'avez pas d'instance en main. Ils sont beaucoup utilisés, et il était vraiment fastidieux de les appliquer sans la syntaxe de décorateur.

Memoization

Le décorateur @memoize se souvient du résultat du premier appel d'une fonction pour un ensemble de paramètres particulier et le met en cache. Les invocations suivantes avec les mêmes paramètres renvoient le résultat mis en cache. 

Cela pourrait considérablement améliorer les performances des fonctions qui effectuent des traitements coûteux (par exemple, contacter une base de données distante ou appeler plusieurs API REST) ​​et sont appelées souvent avec les mêmes paramètres..

@memoize def fetch_data (items): "" "Faites un travail sérieux ici" "" result = [fetch_item_data (i) for i in items] renvoie le résultat 

Programmation contractuelle

Que diriez-vous de deux décorateurs appelés @precondition et @postcondition pour valider l'argument d'entrée ainsi que le résultat? Considérons la fonction simple suivante:

def add_small ints (a, b): "" "Ajoute deux ints dont la somme est toujours un int" "" return a + b

Si quelqu'un l'appelle avec de grands entiers ou des chaînes longues, voire longues, il réussira discrètement, mais le contrat stipule que le résultat doit être un entier. Si quelqu'un l'appelle avec des types de données incompatibles, vous obtiendrez une erreur d'exécution générique. Vous pouvez ajouter le code suivant à la fonction:

def add_small ints (a, b): "" "Ajoute deux ints dans la somme dont le montant est toujours un int" "" assert (isinstance (a, int), 'a doit être un int') assert (isinstance (a, int ), 'b doit être un int') result = a + b assert (isinstance (result, int), les arguments sont trop grands. sum n'est pas un int ') return 

Notre belle ligne add_small_ints () la fonction vient de devenir un vilain bourbier avec des affirmations laides. Dans une fonction réelle, il peut être très difficile de voir d'un coup d'œil ce qu'elle fait. Avec les décorateurs, les conditions pré et post peuvent sortir du corps de la fonction:

@precondition (isinstance (a, int), 'a doit être un int') @precondition (isinstance (b, int), 'b doit être un int') @postcondition (isinstance (retval, int), 'les arguments sont trop grosse. la somme n'est pas un entier ') def add_small ints (a, b): "" "Ajoute deux ints dans la somme dont la somme est toujours un" "" retourne a + b 

Autorisation

Supposons que vous ayez une classe nécessitant une autorisation via un secret pour toutes ses nombreuses méthodes. En tant que développeur consommé de Python, vous opterez probablement pour un décorateur de méthodes @authorized comme dans:

classe SuperSecret (objet): @authorized def f_1 (* args, secret): "" "" "" @authorized def f_2 (* args, secret): "" "" "" "… @authorized def f_100 (* args, secret ): "" "" ""

C'est certainement une bonne approche, mais il est un peu agaçant de le faire de manière répétitive, surtout si vous en avez beaucoup.. 

Plus important encore, si quelqu'un ajoute une nouvelle méthode et oublie d'ajouter la décoration @authorized, vous avez un problème de sécurité sur les mains. N'ai pas peur. Les décorateurs de la classe Python 3 ont votre dos. La syntaxe suivante vous permettra (avec la définition appropriée du décorateur de classe) d'autoriser automatiquement chaque méthode des classes cibles:


@authorized class SuperSecret (objet): def f_1 (* args, secret): "" "" "" def f_2 (* args, secret): "" "" "" "… def f_100 (* args, secret):" " "" ""

Tout ce que vous avez à faire est de décorer la classe elle-même. Notez que le décorateur peut être intelligent et ignorer une méthode spéciale telle que __init __ () ou peut être configuré pour s’appliquer à un sous-ensemble particulier si nécessaire. Le ciel (ou votre imagination) est la limite.

Plus d'exemples

Si vous souhaitez poursuivre d'autres exemples, consultez la PythonDecoratorLibrary. 

Qu'est-ce qu'un décorateur??

Maintenant que vous avez vu quelques exemples en action, il est temps de dévoiler la magie. La définition formelle est qu'un décorateur est un appelable qui accepte un appelable (la cible) et renvoie un appelable (le décoré) qui accepte les mêmes arguments que la cible d'origine.. 

Woah! c'est beaucoup de mots empilés les uns sur les autres de manière incompréhensible. Tout d'abord, qu'est-ce qu'un callable? Un callable est juste un objet Python qui a un __appel__() méthode. Ce sont généralement des fonctions, des méthodes et des classes, mais vous pouvez implémenter une __appel__() méthode sur l’une de vos classes et vos instances de classe deviendront également des callables. Pour vérifier si un objet Python est appelable, vous pouvez utiliser la fonction intégrée callable ():


callable (len) True callable ('123') False

Notez que le callable () Cette fonction a été supprimée de Python 3.0 et ramenée dans Python 3.2. Par conséquent, si pour une raison quelconque vous utilisez Python 3.0 ou 3.1, vous devez vérifier l'existence du __appel__ attribuer comme dans hasattr (len, '__call__').

Lorsque vous prenez un tel décorateur et que vous l'appliquez à l'aide de la syntaxe @ à un callable, le callable d'origine est remplacé par le callable renvoyé par le décorateur. Cela peut être un peu difficile à comprendre, alors illustrons-le en regardant dans les tripes de simples décorateurs..

Décorateurs de fonction

Un décorateur de fonction est un décorateur utilisé pour décorer une fonction ou une méthode. Supposons que nous voulions imprimer la chaîne "Ouais, ça marche!" chaque fois qu'une fonction ou une méthode décorée est appelée avant d'appeler réellement la fonction d'origine. Voici un moyen non-décorateur d'y parvenir. Voici la fonction foo () qui affiche "foo () ici":

def foo (): affiche 'foo () ici' foo () Sortie: foo () ici 

Voici le mauvais moyen d'obtenir le résultat souhaité:

original_foo = toto def created_foo (): print 'Ouais, ça marche!' original_foo () foo = decorated_foo foo () Sortie: Ouais, ça marche! foo () ici

Il y a plusieurs problèmes avec cette approche:

  • C'est beaucoup de travail.
  • Vous polluez l'espace de noms avec des noms intermédiaires tels que original_foo () et décoré_foo ().
  • Vous devez le répéter pour chaque autre fonction que vous souhaitez décorer avec la même capacité.

Un décorateur qui réalise le même résultat et qui est réutilisable et composable ressemble à ceci:

def yeah_it_works (f): def decoré (* args, ** kwargs): print 'Ouais, ça marche' return f (* args, ** kwargs) retour décoré

Notez que yeah_it_works () est une fonction (donc callable) qui accepte un callable ** f ** en tant qu'argument et renvoie un callable (la fonction imbriquée ** ** décorée **) qui accepte n'importe quel nombre et type d'arguments..

Maintenant, nous pouvons l'appliquer à n'importe quelle fonction:


@yeah_it_works def f1 () affiche 'f1 () ici' @yeah_it_works def f2 () affiche 'f3 () ici' @yeah_it_works def f3 () affiche 'f3 () ici' f1 () f2 () f3 () Sortie: Ouais, ça marche f1 () ici Ouais, ça marche f2 () ici Ouais, ça marche f3 () ici

Comment ça marche? L'original f1, f2 et f3 fonctions ont été remplacées par la fonction imbriquée décorée renvoyée par yeah_it_works. Pour chaque fonction individuelle, le capturé F callable est la fonction d'origine ( f1f2 ou f3), donc la fonction décorée est différente et fait le bon choix, à savoir imprimer "Ouais, ça marche!" puis invoquer la fonction d'origine F.

Décorateurs de classe

Les décorateurs de classe travaillent à un niveau supérieur et décorent toute une classe. Leur effet a lieu au moment de la définition de la classe. Vous pouvez les utiliser pour ajouter ou supprimer des méthodes de toute classe décorée ou même pour appliquer des décorateurs de fonctions à tout un ensemble de méthodes.. 

Supposons que nous voulions garder une trace de toutes les exceptions déclenchées par une classe particulière dans un attribut de classe. Supposons que nous ayons déjà un décorateur de fonction appelé track_exceptions_decorator qui effectue cette fonctionnalité. Sans décorateur de classe, vous pouvez l'appliquer manuellement à chaque méthode ou recourir à des métaclasses. Par exemple:


classe A (objet): @track_exceptions_decorator def f1 ():… @track_exceptions_decorator def f2 ():… @track_exceptions_decorator def f100 ():… 

Un décorateur de classe qui obtient le même résultat est:


def track_exception (cls): # Obtenir tous les attributs appelables de la classe callable_attributes = k: v pour k, v dans cls .__ dict __. items () si callable (v) # Décorez chaque attribut appelable de la classe d'entrée pour le nom , func dans callable_attributes.items (): decorated = track_exceptions_decorator (func) setattr (cls, nom, décoré) retourne cls @track_exceptions classe A: def f1 (self): print ('1') def f2 (self): print ( '2')

Conclusion

Python est bien connu pour sa flexibilité. Les décorateurs l'amènent au prochain niveau. Vous pouvez intégrer des problèmes transversaux dans des décorateurs réutilisables et les appliquer à des fonctions, des méthodes et des classes entières. Je recommande vivement à tout développeur Python sérieux de se familiariser avec les décorateurs et de tirer pleinement parti de leurs avantages..