2009-08-17 8 views
13

J'ai essayé de créer un décorateur qui peut être utilisé avec les fonctions et les méthodes en python. Ce n'est pas si difficile, mais lors de la création d'un décorateur qui prend des arguments, il semble que ce soit.Utiliser le même décorateur (avec des arguments) avec des fonctions et des méthodes

class methods(object): 
    def __init__(self, *_methods): 
     self.methods = _methods 

    def __call__(self, func): 
     def inner(request, *args, **kwargs): 
      print request 
      return func(request, *args, **kwargs) 
     return inner 

    def __get__(self, obj, type=None): 
     if obj is None: 
      return self 
     new_func = self.func.__get__(obj, type) 
     return self.__class__(new_func) 

Le code ci-dessus enveloppements correctement la fonction/méthode, mais dans le cas d'un procédé, l'argument request est le cas, il fonctionne sur, et non pas le premier argument non-soi.

Existe-t-il un moyen de dire si le décorateur est appliqué à une fonction plutôt qu'à une méthode, et traiter en conséquence?

Répondre

12

Pour développer l'approche __get__. Cela peut être généralisé dans un décorateur décorateur.

class _MethodDecoratorAdaptor(object): 
    def __init__(self, decorator, func): 
     self.decorator = decorator 
     self.func = func 
    def __call__(self, *args, **kwargs): 
     return self.decorator(self.func)(*args, **kwargs) 
    def __get__(self, instance, owner): 
     return self.decorator(self.func.__get__(instance, owner)) 

def auto_adapt_to_methods(decorator): 
    """Allows you to use the same decorator on methods and functions, 
    hiding the self argument from the decorator.""" 
    def adapt(func): 
     return _MethodDecoratorAdaptor(decorator, func) 
    return adapt 

De cette façon, vous pouvez simplement faire votre décorateur adapter automatiquement aux conditions qu'il est utilisé dans.

def allowed(*allowed_methods): 
    @auto_adapt_to_methods 
    def wrapper(func): 
     def wrapped(request): 
      if request not in allowed_methods: 
       raise ValueError("Invalid method %s" % request) 
      return func(request) 
     return wrapped 
    return wrapper 

Notez que la fonction enveloppe est appelée sur tous les appels de fonction, donc ne pas faire rien de cher là-bas.

Utilisation du décorateur:

class Foo(object): 
    @allowed('GET', 'POST') 
    def do(self, request): 
     print "Request %s on %s" % (request, self) 

@allowed('GET') 
def do(request): 
    print "Plain request %s" % request 

Foo().do('GET') # Works 
Foo().do('POST') # Raises 
+0

Vous devez ajouter 'update_wrapper (self, func)' au * début * de '_MethodDecoratorAdaptor .__ init__' (où update_wrapper est amusant module ctools). Cela permet aux décorateurs qui en résultent de conserver des attributs personnalisés sur les fonctions/callables qu'ils décorent, tout en les gardant également composables. – spookylukey

+5

J'ai découvert que cette méthode ne fonctionne que dans certaines circonstances, et est extrêmement difficile à déboguer quand cela ne fonctionne pas. http://groups.google.com/group/django-developers/msg/f36976f5cfbcbeb3 – spookylukey

+0

@spookylukey En fait, la manière dont cela est géré dans Django est également très propre. – astrojuanlu

4

Le décorateur est toujours appliqué à un objet de fonction - avoir le décorateur print le type de son argument et vous serez en mesure de confirmer que; et il devrait généralement retourner un objet fonction, qui est déjà un décorateur avec le bon __get__! -) bien qu'il y ait des exceptions à ce dernier.

i.e., dans le code:

class X(object): 

    @deco 
    def f(self): pass 

deco(f) est appelé dans le corps de la classe, et, pendant que vous êtes toujours là, f est une fonction, pas une instance d'un type de méthode. (La méthode est fabriquée et renvoyée en f__get__ lorsque plus tard f est accédée en tant qu'attribut de X ou une instance de celle-ci). Peut-être que vous pouvez mieux expliquer une utilisation de jouet que vous souhaitez pour votre décorateur, afin que nous puissions vous aider davantage ...?

Modifier: cela va pour les décorateurs avec des arguments aussi, à savoir

class X(object): 

    @deco(23) 
    def f(self): pass 

alors il est deco(23)(f) qui est appelé dans le corps de la classe, f est encore un objet fonction lorsqu'elle est transmise comme argument à ce que appelable deco(23) renvoie, et que callable devrait toujours retourner un objet fonction (généralement - avec des exceptions ;-).

4

Étant donné que vous définissez déjà un __get__ pour utiliser votre décorateur sur la méthode liée, vous pouvez lui transmettre un indicateur lui indiquant s'il est utilisé pour une méthode ou une fonction.

class methods(object): 
    def __init__(self, *_methods, called_on_method=False): 
     self.methods = _methods 
     self.called_on_method 

    def __call__(self, func): 
     if self.called_on_method: 
      def inner(self, request, *args, **kwargs): 
       print request 
       return func(request, *args, **kwargs) 
     else: 
      def inner(request, *args, **kwargs): 
       print request 
       return func(request, *args, **kwargs) 
     return inner 

    def __get__(self, obj, type=None): 
     if obj is None: 
      return self 
     new_func = self.func.__get__(obj, type) 
     return self.__class__(new_func, called_on_method=True) 
1

Une solution partielle (spécifique) que j'ai trouvée repose sur la gestion des exceptions. J'essaye de créer un décorateur pour autoriser seulement certaines méthodes de HttpRequest, mais le faites fonctionner avec les deux fonctions qui sont des vues, et des méthodes qui sont des vues.

Ainsi, cette classe fera ce que je veux:

class methods(object): 
    def __init__(self, *_methods): 
     self.methods = _methods 

    def __call__(self, func): 
     @wraps(func) 
     def inner(*args, **kwargs): 
      try: 
       if args[0].method in self.methods: 
        return func(*args, **kwargs) 
      except AttributeError: 
       if args[1].method in self.methods: 
        return func(*args, **kwargs) 
      return HttpResponseMethodNotAllowed(self.methods) 
     return inner 

Voici les deux cas d'utilisation: la décoration d'une fonction:

@methods("GET") 
def view_func(request, *args, **kwargs): 
    pass 

et les méthodes de décoration d'une classe:

class ViewContainer(object): 
    # ... 

    @methods("GET", "PUT") 
    def object(self, request, pk, *args, **kwargs): 
     # stuff that needs a reference to self... 
     pass 

Existe-t-il une meilleure solution que d'utiliser la gestion des exceptions?

0

Voici une manière générale, je trouve pour détecter si un appelable décoré est une fonction ou méthode:

import functools 

class decorator(object): 

    def __init__(self, func): 
    self._func = func 
    self._obj = None 
    self._wrapped = None 

    def __call__(self, *args, **kwargs): 
    if not self._wrapped: 
     if self._obj: 
     self._wrapped = self._wrap_method(self._func) 
     self._wrapped = functools.partial(self._wrapped, self._obj) 
     else: 
     self._wrapped = self._wrap_function(self._func) 
    return self._wrapped(*args, **kwargs) 

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

    def _wrap_method(self, method): 
    @functools.wraps(method) 
    def inner(self, *args, **kwargs): 
     print('Method called on {}:'.format(type(self).__name__)) 
     return method(self, *args, **kwargs) 
    return inner 

    def _wrap_function(self, function): 
    @functools.wraps(function) 
    def inner(*args, **kwargs): 
     print('Function called:') 
     return function(*args, **kwargs) 
    return inner 

Exemple d'utilisation:

class Foo(object): 
    @decorator 
    def foo(self, foo, bar): 
    print(foo, bar) 

@decorator 
def foo(foo, bar): 
    print(foo, bar) 

foo(12, bar=42)  # Function called: 12 42 
foo(12, 42)   # Function called: 12 42 
obj = Foo() 
obj.foo(12, bar=42) # Method called on Foo: 12 42 
obj.foo(12, 42)  # Method called on Foo: 12 42 
Questions connexes