web-dev-qa-db-fra.com

Python Class Based Decorator avec des paramètres qui peuvent décorer une méthode ou une fonction

J'ai vu de nombreux exemples de décorateurs Python qui sont:

  • décorateurs de style de fonction (habillage d'une fonction)
  • décorateurs de style de classe (implémentation de __init__, __get__, et __call__)
  • décorateurs qui ne prennent pas d'arguments
  • décorateurs qui prennent des arguments
  • les décorateurs qui sont "conviviaux pour les méthodes" (c'est-à-dire qui peuvent décorer une méthode dans une classe)
  • décorateurs qui sont "fonctionnels" (peuvent décorer une fonction simple
  • décorateurs qui peuvent décorer à la fois les méthodes et les fonctions

Mais je n'ai jamais vu un seul exemple qui puisse faire tout ce qui précède, et j'ai du mal à synthétiser à partir de diverses réponses à des questions spécifiques (telles que celle-ci , celle-ci , ou celui-ci (qui a l'une des meilleures réponses que j'ai jamais vues sur SO) ), comment combiner tout cela.

Ce que je veux, c'est un basé sur la classe décorateur qui peut décorer soit une méthode ou une fonction , et qui prend au moins un paramètre supplémentaire . C'est-à-dire pour que les éléments suivants fonctionnent:

class MyDecorator(object):
    def __init__(self, fn, argument):
        self.fn = fn
        self.arg = argument

    def __get__(self, ....):
        # voodoo magic for handling distinction between method and function here

    def __call__(self, *args, *kwargs):
        print "In my decorator before call, with arg %s" % self.arg
        self.fn(*args, **kwargs)
        print "In my decorator after call, with arg %s" % self.arg


class Foo(object):
    @MyDecorator("foo baby!")
    def bar(self):
        print "in bar!"


@MyDecorator("some other func!")
def some_other_function():
    print "in some other function!"

some_other_function()
Foo().bar()

Et je m'attendrais à voir:

In my decorator before call, with arg some other func!
in some other function!
In my decorator after call, with arg some other func!
In my decorator before call, with arg foo baby!
in bar!
In my decorator after call, with arg foo baby!

Edit: si cela importe, j'utilise Python 2.7.

41
Adam Parkin

Vous n'avez pas besoin de jouer avec les descripteurs. Il suffit de créer une fonction wrapper à l'intérieur de la méthode __call__() et de la renvoyer. Standard Python peuvent toujours agir comme une méthode ou une fonction, selon le contexte:

class MyDecorator(object):
    def __init__(self, argument):
        self.arg = argument

    def __call__(self, fn):
        @functools.wraps(fn)
        def decorated(*args, **kwargs):
            print "In my decorator before call, with arg %s" % self.arg
            fn(*args, **kwargs)
            print "In my decorator after call, with arg %s" % self.arg
        return decorated

Un peu d'explication sur ce qui se passe lorsque ce décorateur est utilisé comme ceci:

@MyDecorator("some other func!")
def some_other_function():
    print "in some other function!"

La première ligne crée une instance de MyDecorator et passe "some other func!" Comme argument à __init__(). Appelons cette instance my_decorator. Ensuite, l'objet de fonction non décoré - appelons-le bare_func - est créé et passé à l'instance de décorateur, donc my_decorator(bare_func) est exécuté. Cela invoquera MyDecorator.__call__(), qui créera et renverra une fonction wrapper. Enfin, cette fonction wrapper est affectée au nom some_other_function.

38
Sven Marnach

Vous manquez un niveau.

Considérez le code

class Foo(object):
    @MyDecorator("foo baby!")
    def bar(self):
        print "in bar!"

Il est identique à ce code

class Foo(object):
    def bar(self):
        print "in bar!"
    bar = MyDecorator("foo baby!")(bar)

Alors MyDecorator.__init__ est appelé avec "foo baby!" puis l'objet MyDecorator est appelé avec la fonction bar.

Vous voulez peut-être mettre en œuvre quelque chose de plus comme

import functools

def MyDecorator(argument):
    class _MyDecorator(object):
        def __init__(self, fn):
            self.fn = fn

        def __get__(self, obj, type=None):
            return functools.partial(self, obj)

        def __call__(self, *args, **kwargs):
            print "In my decorator before call, with arg %s" % argument
            self.fn(*args, **kwargs)
            print "In my decorator after call, with arg %s" % argument

    return _MyDecorator
11
Mike Graham

Dans votre liste de types de décorateurs, vous avez raté des décorateurs qui peuvent ou non prendre des arguments. Je pense que cet exemple couvre tous vos types à l'exception des "décorateurs de style de fonction (encapsuler une fonction)"

class MyDecorator(object):

    def __init__(self, argument):
        if hasattr('argument', '__call__'):
            self.fn = argument
            self.argument = 'default foo baby'
        else:
            self.argument = argument

    def __get__(self, obj, type=None):
        return functools.partial(self, obj)

    def __call__(self, *args, **kwargs):
        if not hasattr(self, 'fn'):
            self.fn = args[0]
            return self
        print "In my decorator before call, with arg %s" % self.argument
        self.fn(*args, **kwargs)
        print "In my decorator after call, with arg %s" % self.argument


class Foo(object):
    @MyDecorator("foo baby!")
    def bar(self):
        print "in bar!"

class Bar(object):
    @MyDecorator
    def bar(self):
        print "in bar!"

@MyDecorator
def add(a, b):
    print a + b
5