2009-08-17 14 views
13

Ho cercato di creare un decoratore che possa essere utilizzato con entrambe le funzioni e i metodi in python. Questo da solo non è difficile, ma quando si crea un decoratore che prende argomenti, sembra che lo sia.Utilizzo dello stesso decoratore (con argomenti) con funzioni e metodi

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) 

Il codice sopra avvolge la funzione/metodo corretto, ma nel caso di un metodo, l'argomento request è l'istanza è operativo su, non il primo argomento non-self.

C'è un modo per dire se il decoratore viene applicato a una funzione anziché a un metodo e si occupa di conseguenza?

risposta

12

Per espandere l'approccio __get__. Questo può essere generalizzato in un decoratore decoratore.

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 

In questo modo si può solo rendere il vostro decoratore adattarsi automaticamente alle condizioni viene utilizzato in.

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 

Si noti che la funzione wrapper è chiamato per tutte le chiamate di funzione, in modo da non fare qualcosa di costoso lì.

Uso del decoratore:

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

Si dovrebbe aggiungere 'update_wrapper (self, func)' al * inizio * di '_MethodDecoratorAdaptor .__ init__' (dove update_wrapper è dal divertimento modulo ctools). Questo fa sì che i decoratori risultanti conservino gli attributi personalizzati sulle funzioni/callebles che decorano, mantenendoli anche componibili. – spookylukey

+5

Ho scoperto che questo metodo funziona solo in alcune circostanze ed è estremamente difficile eseguire il debug quando non funziona. http://groups.google.com/group/django-developers/msg/f36976f5cfbcbeb3 – spookylukey

+0

@spookylukey In realtà il modo in cui questo viene gestito in Django è abbastanza pulito. – astrojuanlu

4

Il decoratore viene sempre applicato a un oggetto funzione - ha il decoratore print il tipo di argomento e sarà in grado di confermarlo; e dovrebbe generalmente restituire anche un oggetto funzione (che è già un decoratore con l'appropriato __get__! -) sebbene ci siano eccezioni a quest'ultimo.

cioè, nel codice:

class X(object): 

    @deco 
    def f(self): pass 

deco(f) è chiamato all'interno del corpo della classe, e, mentre siete ancora lì, f è una funzione, non è un'istanza di un tipo di metodo. (Il metodo viene prodotto e restituito in quando viene effettuato l'accesso a f come attributo di X o un'istanza di esso).

Forse potresti spiegare meglio l'uso di un giocattolo che vorresti per il tuo decoratore, quindi possiamo essere di maggiore aiuto ...?

Modifica: questo vale per decoratori con argomenti, troppo, vale a dire

class X(object): 

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

allora è deco(23)(f) che si chiama nel corpo della classe, f è ancora un oggetto funzione quando viene passato come argomento a qualsiasi callable deco(23) ritorna, e quel callable dovrebbe comunque restituire un oggetto funzione (generalmente - con eccezioni ;-).

4

Poiché si sta già definendo un __get__ per utilizzare il decoratore sul metodo associato, è possibile passare una bandiera che indica se viene utilizzata su un metodo o funzione.

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 soluzione parziale (specifica) che ho trovato si basa sulla gestione delle eccezioni. Sto tentando di creare un decoratore per consentire solo determinati metodi HttpRequest, ma farlo funzionare con entrambe le funzioni che sono viste e metodi che sono viste.

Quindi, questa classe farà quello che voglio:

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 

Qui ci sono i due casi d'uso: decorazione di una funzione:

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

e decorazione metodi di una classe:

class ViewContainer(object): 
    # ... 

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

Esiste una soluzione migliore rispetto all'utilizzo della gestione delle eccezioni?

0

Ecco un modo generale ho trovato per rilevare se un richiamabile decorato è una funzione o metodo:

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 

Utilizzo Esempio:

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