2012-01-23 20 views
6

Tuve un problema interesante esta mañana. Tenía una clase base que se veía así:Decorar un método que ya es un método de clase?

# base.py 
class Base(object): 

    @classmethod 
    def exists(cls, **kwargs): 
     # do some work 
     pass 

y un módulo de decorador que se veía así:

# caching.py 

# actual caching decorator 
def cached(ttl): 
    # complicated 

def cached_model(ttl=300): 
    def closure(model_class): 
     # ... 
     # eventually: 
     exists_decorator = cached(ttl=ttl) 
     model_class.exists = exists_decorator(model_class.exists)) 

     return model_class 
    return closure 

Aquí es mi modelo de subclase:

@cached_model(ttl=300) 
class Model(Base): 
    pass 

La cosa es que cuando en realidad llame a Model.exists, ¡recibo quejas sobre la cantidad incorrecta de argumentos! Inspeccionar los argumentos en el decorador no muestra nada extraño: los argumentos son exactamente lo que espero y coinciden con la firma del método. ¿Cómo puedo agregar más decoradores a un método que ya está decorado con classmethod?

No todos los modelos se almacenan en caché, pero el exists() método está presente en todos los modelos como classmethod, por lo que volver a ordenar los decoradores no es una opción: cached_model puede agregar a classmethod existe(), pero luego lo hace Existe() ¿un método de clase en modelos no almacenados?

+1

Entonces, ¿cuál es la solución? No es claro. Hubiera sido mucho mejor si hubieras dejado tu pregunta tal como estaba, y hubieras enviado una respuesta. – Marcin

+1

* Puede * publicar una pregunta y responderla usted mismo, pero mantenga las preguntas y respuestas separadas. Ver http://meta.stackexchange.com/questions/17463/can-i-answer-my-own-questions-even-those-where-i-knew-the-answer-before-asking – delnan

+0

Creo que olvidó una '@ classmethod' en la clase' Base'. –

Respuesta

1

El decorador classmethod realmente antepone un argumento class a las llamadas al método, en determinadas circunstancias, hasta donde puedo decir, además de vincular el método a la clase.La solución estaba editando mi clase de decoración de cierre:

def cached_model(ttl=300): 
    def closure(model_class): 
     # ... 
     # eventually: 
     exists_decorator = cached(ttl=ttl, cache_key=exists_cache_key) 
     model_class.exists = classmethod(exists_decorator(model_class.exists.im_func)) 

     return model_class 
    return closure 

La propiedad im_func aparece para obtener una referencia a la función original, lo que me permite meter la mano y decorar la función original con mi decorador de almacenamiento en caché y, a continuación, envolver que toda lío en una llamada classmethod. Resumen, las decoraciones classmethod no son apilables, porque los argumentos parecen estar inyectados.

5

En Python, cuando se declara un método, en un cuerpo de función, es exactamente como una función - una vez que la clase se analiza y existe, recuperando el método a través del "." el operador transforma esa función, sobre la marcha, en un método. Esta transformada de no añadir el primer parámetro con el método (si no es un métodoestático) -

así:

>>> class A(object): 
... def b(self): 
...  pass 
... 
>>> A.b is A.b 
False 

robaba cada recuperación del atributo "b" de "A" produce una instancia diferente de el "método de objeto b"

>>> A.b 
<unbound method A.b> 

la función original "b" puede ser recuperada sin ningún trasnform si uno hace

>>> A.__dict__["b"] 
<function b at 0xe36230> 

Para una función decorado con @classmethod lo mismo sucede, y la "clase" valor se añade a la lista de parámetros cuando se recupera de A.

Los @classmethod y decoradores @staticmethod envolverá la función subyacente en un descriptor diferente que el método de instancia normal. Un objeto classmethod - que es una función cuando se envuelve con classmethod es un objeto descriptor, que tiene un método '__get__' que devolverá una función envolviendo la función subyacente y añadiendo el parámetro "cls" antes que todos los demás .

Cualquier otro decorador a @classmethod tiene que "saber" que en realidad se trata de un objeto descriptor, no una función. -

>>> class A(object): 
... @classmethod 
... def b(cls): 
...  print b 
... 
>>> A.__dict__["b"] 
<classmethod object at 0xd97a28> 

lo tanto, es mucho más fácil dejar que el @classmethod decorador para ser el último en ser aplicado con el método (el primero en la pila) - de modo que los otros decoradores trabajan en una función simple (sabiendo que el argumento "cls" se insertará como el primero).

+1

Cierto, pero a veces no se tiene control sobre el orden de los decoradores. En mi caso, el método siempre es un método de clase, pero solo algunas clases tienen el decorador de caché adicional, es específico de una subclase. En ese caso, necesita una forma de 'deshacer' el decorador de primer clase. Ver mi respuesta para lo que terminé yendo. –

2

Gracias a jsbueno para obtener información sobre Python. Estaba buscando una respuesta a esta pregunta basada en el caso de decorating all methods of a class. Basado en busca de una respuesta a esta pregunta y Reponse de jsbueno, yo era capaz de reunir algo en la línea de:

def for_all_methods(decorator): 

    def decorate(cls): 

     for attr in dir(cls): 
      possible_method = getattr(cls, attr) 
      if not callable(possible_method): 
       continue 

      # staticmethod 
      if not hasattr(possible_method, "__self__"): 
       raw_function = cls.__dict__[attr].__func__ 
       decorated_method = decorator(raw_function) 
       decorated_method = staticmethod(decorated_method) 

      # classmethod 
      elif type(possible_method.__self__) == type: 
       raw_function = cls.__dict__[attr].__func__ 
       decorated_method = decorator(raw_function) 
       decorated_method = classmethod(decorated_method) 

      # instance method 
      elif possible_method.__self__ is None: 
       decorated_method = decorator(possible_method) 

      setattr(cls, attr, decorated_method) 

     return cls 
    return decorate 

Hay un poco de redundancia y algunas variaciones que puede utilizar para cortar esto un poco.

0

Sólo un ejemplo de función para añadir a gran respuesta de Scott Lobdell ...

messages.py

from distutils.cmd import Command 

import functools 
import unittest 

def for_all_methods(decorator): 

    def decorate(cls): 

     for attr in cls.__dict__: 
      possible_method = getattr(cls, attr) 
      if not callable(possible_method): 
       continue 

      # staticmethod 
      if not hasattr(possible_method, "__self__"): 
       raw_function = cls.__dict__[attr].__func__ 
       decorated_method = decorator(raw_function) 
       decorated_method = staticmethod(decorated_method) 

      # classmethod 
      if type(possible_method.__self__) == type: 
       raw_function = cls.__dict__[attr].__func__ 
       decorated_method = decorator(raw_function) 
       decorated_method = classmethod(decorated_method) 


      # instance method 
      elif possible_method.__self__ is None: 
       decorated_method = decorator(possible_method) 

      setattr(cls, attr, decorated_method) 

     return cls 

    return decorate 

def add_arguments(func): 
    """ 
    The add_arguments decorator simply add the passed in arguments 
    (args and kwargs) the returned error message. 
    """  
    @functools.wraps(func) 
    def wrapped(self, *args, **kwargs): 
     try: 
      message = func(self, *args, **kwargs) 
      message = ''.join([message, 
           "[ args:'", str(args), "'] ", 
           "[ kwargs:'", str(kwargs), "' ] " 
           ]) 
      return message 

     except Exception as e: 
      err_message = ''.join(["errorhandler.messages.MESSAGE: '", 
            str(func), 
            "(", str(args), str(kwargs), ")' ", 
            "FAILED FOR UNKNOWN REASON. ", 
            " [ ORIGINAL ERROR: ", str(e), " ] " 
            ]) 
      return err_message 

    return wrapped 



@for_all_methods(add_arguments)  
class MESSAGE(object): 
    """ 
      log.error(MSG.triggerPhrase(args, kwargs)) 

    """  
    @classmethod 
    def TEMPLATE(self, *args, **kwargs): 
     message = "This is a template of a pre-digested message." 
     return message 

uso

from messages import MESSAGE 

if __name__ == '__main__': 
    result = MESSAGE.TEMPLATE(1,2,test=3) 
    print result 

salida

This is a template of a pre-digested message.[ args:'(1, 2)'] [ kwargs:'{'test': 3}' ] 
Cuestiones relacionadas