2009-08-17 18 views
13

He estado tratando de crear un decorador que se pueda usar con funciones y métodos en python. Esto por sí solo no es tan difícil, pero cuando se crea un decorador que toma argumentos, parece serlo.Usando el mismo decorador (con argumentos) con funciones y métodos

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) 

El código anterior envuelve la función/método correctamente, pero en el caso de un método, el argumento request es la instancia que está operando en, no el primer argumento no autónomos.

¿Hay alguna manera de saber si el decorador se está aplicando a una función en lugar de a un método, y tratar en consecuencia?

Respuesta

12

Para ampliar el enfoque __get__. Esto se puede generalizar en un decorador decorador.

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 esta manera usted puede hacer que su decorador se adaptan automáticamente a las condiciones que se utiliza en.

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 

en cuenta que la función de contenedor se pidió a todas las llamadas a funciones, por lo que no hacer algo caro allí.

Uso del decorador:

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

Debería agregar 'update_wrapper (self, func)' al * comienzo * de '_MethodDecoratorAdaptor .__ init__' (donde update_wrapper es de diversión módulo ctools). Esto hace que los decoradores resultantes conserven atributos personalizados en las funciones/callables que decoran, a la vez que los mantiene también compostables. – spookylukey

+5

Descubrí que este método solo funciona en algunas circunstancias y es extremadamente difícil depurarlo cuando no funciona. http://groups.google.com/group/django-developers/msg/f36976f5cfbcbeb3 – spookylukey

+0

@spookylukey De hecho, la forma en que esto se maneja en Django es bastante limpia. – astrojuanlu

4

El decorador siempre se aplica a un objeto de función; tenga el decorador print el tipo de argumento y podrá confirmarlo; y generalmente debería devolver también un objeto de función (que ya es un decorador con el __get__ apropiado -) aunque hay excepciones a este último.

es decir, en el código:

class X(object): 

    @deco 
    def f(self): pass 

deco(f) se llama dentro del cuerpo de la clase, y, mientras está todavía allí, f es una función y no una instancia de un tipo de método. (El método se fabrica y devuelve en f 's __get__ cuando más tarde f se accede como un atributo de X o una instancia de la misma).

Tal vez puedas explicar mejor el uso de un juguete que quieras para tu decorador, ¿así que podemos ser de más ayuda ...?

Editar: esto va para decoradores con argumentos, demasiado, es decir

class X(object): 

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

entonces es deco(23)(f) que se llama en el cuerpo de la clase, f sigue siendo un objeto de función cuando se pasa como argumento a lo que se puede llamar deco(23) regresa, y ese invocable aún debe devolver un objeto de función (generalmente, con excepciones ;-).

4

Dado que ya está definiendo un __get__ para usar su decorador en el Método Bound, podría pasar un indicador indicándole si se está utilizando en un método o función.

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

Una solución parcial (específico) que he subido con se basa en el manejo de excepciones. Estoy intentando crear un decorador para permitir solo ciertos métodos de HttpRequest, pero hacerlo funcionar con ambas funciones que son vistas y métodos que son vistas.

Por lo tanto, esta clase hará lo que yo quiero:

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 

Éstos son los dos casos de uso: la decoración de una función:

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

y decoración de métodos de una clase:

class ViewContainer(object): 
    # ... 

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

¿Existe una mejor solución que usar el manejo de excepciones?

0

Aquí está una manera general I encontré para detectar si un callable decorado es una función o método:

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 

Ejemplo de uso:

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 
Cuestiones relacionadas