2011-11-18 12 views
7

Me gustaría crear un decorador de clase de Python (*) que pueda ajustar sin problemas todos los tipos de métodos que la clase pueda tener: instancia, clase y estática.¿Cómo crear un decorador de clases de Python capaz de envolver los métodos instancia, clase y estáticos?

Este es el código que tengo por ahora, con las piezas que se rompen comentó:

def wrapItUp(method): 
    def wrapped(*args, **kwargs): 
     print "This method call was wrapped!" 
     return method(*args, **kwargs) 
    return wrapped 

dundersICareAbout = ["__init__", "__str__", "__repr__"]#, "__new__"] 

def doICareAboutThisOne(cls, methodName): 
    return (callable(getattr(cls, methodName)) 
      and (not (methodName.startswith("__") and methodName.endswith("__")) 
      or methodName in dundersICareAbout)) 

def classDeco(cls): 
    myCallables = ((aname, getattr(cls, aname)) for aname in dir(cls) if doICareAboutThisOne(cls, aname)) 
    for name, call in myCallables: 
     print "*** Decorating: %s.%s(...)" % (cls.__name__, name) 
     setattr(cls, name, wrapItUp(call)) 
    return cls 

@classDeco 
class SomeClass(object): 

    def instanceMethod(self, p): 
     print "instanceMethod: p =", p 

    @classmethod 
    def classMethod(cls, p): 
     print "classMethod: p =", p 

    @staticmethod 
    def staticMethod(p): 
     print "staticMethod: p =", p 


instance = SomeClass() 
instance.instanceMethod(1) 
#SomeClass.classMethod(2) 
#instance.classMethod(2) 
#SomeClass.staticMethod(3) 
#instance.staticMethod(3) 

estoy teniendo dos cuestiones que tratan de hacer este trabajo:

  • Cuando iteración sobre todo Callables, ¿cómo puedo averiguar si es de una instancia, clase o tipo estático?
  • ¿Cómo sobrescribo el método con una versión ajustada apropiada que se invoca correctamente para cada uno de esos casos?

Actualmente, este código genera diferentes TypeError s dependiendo de lo comentado fragmento es sin comentar, como:

  • TypeError: unbound method wrapped() must be called with SomeClass instance as first argument (got int instance instead)
  • TypeError: classMethod() takes exactly 2 arguments (3 given)

(*): El mismo el problema es mucho más simple si eres decorating the methods directly.

+0

Será divertido ver cómo diferentes desarrolladores han manejado este mismo y complicado problema. – wberry

+0

@wberry: sí, ya estoy leyendo las respuestas actuales y noté que será difícil elegir la "correcta". – Chuim

Respuesta

4

Dado que los métodos son contenedores para las funciones, para aplicar un decorador a un método en una clase después de la clase ha sido construido, tiene que:

  1. Extracto de la función subyacente del método que usa su atributo im_func.
  2. Decora la función.
  3. Vuelva a aplicar la envoltura.
  4. Sobrescribe el atributo con la función envuelta y decorada.

Es difícil distinguir una classmethod de un método regular una vez que el @classmethod decorador se ha aplicado; ambos tipos de métodos son del tipo instancemethod. Sin embargo, puede verificar el atributo im_self y ver si es None. Si es así, es un método de instancia regular; de lo contrario, es classmethod.

Los métodos estáticos son funciones simples (el decorador @staticmethod simplemente evita que se aplique la envoltura habitual del método). Entonces no tienes que hacer nada especial para esto, parece.

Así que, básicamente, su algoritmo tiene el siguiente aspecto:

  1. obtener el atributo.
  2. ¿Se puede llamar? Si no, proceda al siguiente atributo.
  3. ¿Es de tipo types.MethodType? Si es así, es un método de clase o un método de instancia.
    • Si su im_self es None, es un método de instancia. Extracto de la función subyacente a través del atributo im_func, decorar eso, y volver a aplicar el método de instancia: meth = types.MethodType(func, None, cls)
    • Si su im_self no es None, es un método de clase. Excluya la función subyacente a través del im_func y decore eso. Ahora tiene que volver a aplicar el decorador classmethod, pero no puede porque classmethod() no toma una clase, por lo que no hay forma de especificar a qué clase se va a adjuntar. En su lugar, debe usar el decorador de métodos de instancia: meth = types.MethodType(func, cls, type). Tenga en cuenta que el type aquí es el incorporado, type.
  4. Si su tipo no es types.MethodType, entonces se trata de un método estático u otro no exigible, así que simplemente decodifíquelo.
  5. Restablece el nuevo atributo en la clase.

Estos cambian un tanto en Python 3 - los métodos independientes son funciones allí, IIRC. En cualquier caso, es probable que deba repensarse completamente allí.

+0

Elija esta como la respuesta, ya que explica claramente el problema subyacente. ¡Gracias y perdón por la larga espera! – Chuim

3

Hay una función no documentada, inspect.classify_class_attrs, que le puede indicar qué atributos son classmethods o staticmethods. Debajo del capó, usa isinstance(obj, staticmethod) y isinstance(obj, classmethod) para clasificar los métodos estáticos y de clase. Siguiendo ese patrón, esto funciona tanto en Python2 como en Python3:

def wrapItUp(method,kind='method'): 
    if kind=='static method': 
     @staticmethod 
     def wrapped(*args, **kwargs): 
      return _wrapped(*args,**kwargs) 
    elif kind=='class method': 
     @classmethod 
     def wrapped(cls,*args, **kwargs): 
      return _wrapped(*args,**kwargs)     
    else: 
     def wrapped(self,*args, **kwargs): 
      return _wrapped(self,*args,**kwargs)         
    def _wrapped(*args, **kwargs): 
     print("This method call was wrapped!") 
     return method(*args, **kwargs) 
    return wrapped 
def classDeco(cls): 
    for name in (name 
       for name in dir(cls) 
       if (callable(getattr(cls,name)) 
        and (not (name.startswith('__') and name.endswith('__')) 
          or name in '__init__ __str__ __repr__'.split())) 
       ): 
     method = getattr(cls, name) 
     obj = cls.__dict__[name] if name in cls.__dict__ else method 
     if isinstance(obj, staticmethod): 
      kind = "static method" 
     elif isinstance(obj, classmethod): 
      kind = "class method" 
     else: 
      kind = "method" 
     print("*** Decorating: {t} {c}.{n}".format(
      t=kind,c=cls.__name__,n=name)) 
     setattr(cls, name, wrapItUp(method,kind)) 
    return cls 

@classDeco 
class SomeClass(object): 
    def instanceMethod(self, p): 
     print("instanceMethod: p = {}".format(p)) 
    @classmethod 
    def classMethod(cls, p): 
     print("classMethod: p = {}".format(p)) 
    @staticmethod 
    def staticMethod(p): 
     print("staticMethod: p = {}".format(p)) 

instance = SomeClass() 
instance.instanceMethod(1) 
SomeClass.classMethod(2) 
instance.classMethod(2) 
SomeClass.staticMethod(3) 
instance.staticMethod(3) 
Cuestiones relacionadas