El decorador Python hace que la función olvide que pertenece a una clase

Estoy tratando de escribir un decorador para hacer el registro:

def logger(myFunc): def new(*args, **keyargs): print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__) return myFunc(*args, **keyargs) return new class C(object): @logger def f(): pass C().f() 

Me gustaría que esto se imprima:

 Entering Cf 

pero en su lugar me sale este mensaje de error:

 AttributeError: 'function' object has no attribute 'im_class' 

Es de suponer que esto tiene algo que ver con el scope de ‘myFunc’ dentro de ‘logger’, pero no tengo idea de qué.

La respuesta de Claudiu es correcta, pero también puedes hacer trampa al quitar el nombre de la clase del argumento self . Esto dará declaraciones de registro engañosas en casos de herencia, pero le dirá la clase del objeto cuyo método se está llamando. Por ejemplo:

 from functools import wraps # use this to preserve function signatures and docstrings def logger(func): @wraps(func) def with_logging(*args, **kwargs): print "Entering %s.%s" % (args[0].__class__.__name__, func.__name__) return func(*args, **kwargs) return with_logging class C(object): @logger def f(self): pass C().f() 

Como dije, esto no funcionará correctamente en los casos en que hayas heredado una función de una clase padre; en este caso podrías decir

 class B(C): pass b = B() bf() 

y obtenga el mensaje Entering Bf donde realmente desea recibir el mensaje Entering Cf ya que esa es la clase correcta. Por otro lado, esto podría ser aceptable, en cuyo caso recomendaría este enfoque sobre la sugerencia de Claudiu.

Las funciones solo se convierten en métodos en tiempo de ejecución. Es decir, cuando obtiene Cf obtiene una función vinculada (y Cfim_class is C ). En el momento en que se define su función, es solo una función simple, no está vinculada a ninguna clase. Esta función no vinculada y desasociada es la que está decorada por el registrador.

self.__class__.__name__ le dará el nombre de la clase, pero también puede usar descriptores para lograr esto de una manera algo más general. Este patrón se describe en una publicación de blog sobre Decoradores y Descriptores , y una implementación de su decorador de logger en particular sería:

 class logger(object): def __init__(self, func): self.func = func def __get__(self, obj, type=None): return self.__class__(self.func.__get__(obj, type)) def __call__(self, *args, **kw): print 'Entering %s' % self.func return self.func(*args, **kw) class C(object): @logger def f(self, x, y): return x+y C().f(1, 2) # => Entering > 

Obviamente, la salida se puede mejorar (utilizando, por ejemplo, getattr(self.func, 'im_class', None) ), pero este patrón general funcionará tanto para los métodos como para las funciones. Sin embargo, no funcionará para las clases de estilo antiguo (pero simplemente no las use)

Las ideas propuestas aquí son excelentes, pero tienen algunas desventajas:

  1. inspect.getouterframes and args[0].__class__.__name__ no son adecuados para funciones simples y métodos estáticos.
  2. __get__ debe estar en una clase, que es rechazada por @wraps .
  3. @wraps sí debería estar escondiendo mejor los rastros.

Entonces, he combinado algunas ideas de esta página, enlaces, documentos y mi propia cabeza,
y finalmente encontró una solución, que carece de los tres inconvenientes anteriores.

Como resultado, method_decorator :

  • Conoce la clase a la que está obligado el método decorado.
  • Oculta trazas de decorador respondiendo a los atributos del sistema más correctamente que functools.wraps() .
  • Está cubierto con pruebas unitarias para métodos de instancia enlazados y no enlazados, métodos de clase, métodos estáticos y funciones simples.

Uso:

 pip install method_decorator from method_decorator import method_decorator class my_decorator(method_decorator): # ... 

Ver pruebas unitarias completas para detalles de uso .

Y aquí está el código de la clase method_decorator :

 class method_decorator(object): def __init__(self, func, obj=None, cls=None, method_type='function'): # These defaults are OK for plain functions # and will be changed by __get__() for methods once a method is dot-referenced. self.func, self.obj, self.cls, self.method_type = func, obj, cls, method_type def __get__(self, obj=None, cls=None): # It is executed when decorated func is referenced as a method: cls.func or obj.func. if self.obj == obj and self.cls == cls: return self # Use the same instance that is already processed by previous call to this __get__(). method_type = ( 'staticmethod' if isinstance(self.func, staticmethod) else 'classmethod' if isinstance(self.func, classmethod) else 'instancemethod' # No branch for plain function - correct method_type for it is already set in __init__() defaults. ) return object.__getattribute__(self, '__class__')( # Use specialized method_decorator (or descendant) instance, don't change current instance attributes - it leads to conflicts. self.func.__get__(obj, cls), obj, cls, method_type) # Use bound or unbound method with this underlying func. def __call__(self, *args, **kwargs): return self.func(*args, **kwargs) def __getattribute__(self, attr_name): # Hiding traces of decoration. if attr_name in ('__init__', '__get__', '__call__', '__getattribute__', 'func', 'obj', 'cls', 'method_type'): # Our known names. '__class__' is not included because is used only with explicit object.__getattribute__(). return object.__getattribute__(self, attr_name) # Stopping recursion. # All other attr_names, including auto-defined by system in self, are searched in decorated self.func, eg: __module__, __class__, __name__, __doc__, im_*, func_*, etc. return getattr(self.func, attr_name) # Raises correct AttributeError if name is not found in decorated self.func. def __repr__(self): # Special case: __repr__ ignores __getattribute__. return self.func.__repr__() 

Parece que mientras se crea la clase, Python crea objetos de funciones regulares. Solo se convierten en objetos de métodos no unidos después. Sabiendo eso, esta es la única manera que podría encontrar para hacer lo que quieres:

 def logger(myFunc): def new(*args, **keyargs): print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__) return myFunc(*args, **keyargs) return new class C(object): def f(self): pass Cf = logger(Cf) C().f() 

Esto produce el resultado deseado.

Si desea envolver todos los métodos en una clase, entonces probablemente quiera crear una función wrapClass, que podría usar así:

 C = wrapClass(C) 

Las funciones de clase siempre deben tomarse como primer argumento, así que puedes usar eso en lugar de im_class.

 def logger(myFunc): def new(self, *args, **keyargs): print 'Entering %s.%s' % (self.__class__.__name__, myFunc.__name__) return myFunc(self, *args, **keyargs) return new class C(object): @logger def f(self): pass C().f() 

al principio quería usar self.__name__ pero eso no funciona porque la instancia no tiene nombre. debe usar self.__class__.__name__ para obtener el nombre de la clase.

Encontré otra solución a un problema muy similar utilizando la biblioteca inspect . Cuando se llama al decorador, aunque la función aún no está vinculada a la clase, puede inspeccionar la stack y descubrir qué clase está llamando el decorador. Al menos puede obtener el nombre de la cadena de la clase, si eso es todo lo que necesita (probablemente no pueda hacer referencia a él desde que se creó). Entonces no necesita llamar nada después de que se haya creado la clase.

 import inspect def logger(myFunc): classname = inspect.getouterframes(inspect.currentframe())[1][3] def new(*args, **keyargs): print 'Entering %s.%s' % (classname, myFunc.__name__) return myFunc(*args, **keyargs) return new class C(object): @logger def f(self): pass C().f() 

Si bien esto no es necesariamente mejor que los otros, es la única forma en que puedo descubrir el nombre de la clase del método futuro durante la llamada al decorador. Tome nota de no guardar referencias a marcos alrededor de la documentación de la biblioteca inspect .

También puede usar new.instancemethod() para crear un método de instancia (enlazado o no enlazado) desde una función.

En lugar de inyectar código de decoración en el momento de la definición, cuando la función no conoce su clase, demora la ejecución de este código hasta que se acceda / llame a la función. El objeto descriptor facilita la inyección de código propio en el tiempo de acceso / llamada:

 class decorated(object): def __init__(self, func, type_=None): self.func = func self.type = type_ def __get__(self, obj, type_=None): return self.__class__(self.func.__get__(obj, type_), type_) def __call__(self, *args, **kwargs): name = '%s.%s' % (self.type.__name__, self.func.__name__) print('called %s with args=%s kwargs=%s' % (name, args, kwargs)) return self.func(*args, **kwargs) class Foo(object): @decorated def foo(self, a, b): pass 

Ahora podemos inspeccionar la clase tanto en el tiempo de acceso ( __get__ ) como en el tiempo de llamada ( __call__ ). Este mecanismo funciona tanto para métodos simples como para métodos estáticos de clase:

 >>> Foo().foo(1, b=2) called Foo.foo with args=(1,) kwargs={'b': 2} 

Ejemplo completo en: https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py

Como se muestra en la respuesta de Asa Ayers , no es necesario acceder al objeto de clase. Puede valer la pena saber que desde Python 3.3, también puede usar __qualname__ , que le da el nombre completo:

 >>> def logger(myFunc): ... def new(*args, **keyargs): ... print('Entering %s' % myFunc.__qualname__) ... return myFunc(*args, **keyargs) ... ... return new ... >>> class C(object): ... @logger ... def f(self): ... pass ... >>> C().f() Entering Cf 

Esto tiene la ventaja adicional de trabajar también en el caso de clases anidadas, como se muestra en este ejemplo tomado de PEP 3155 :

 >>> class C: ... def f(): pass ... class D: ... def g(): pass ... >>> C.__qualname__ 'C' >>> Cf__qualname__ 'Cf' >>> CD__qualname__ 'CD' >>> CDg__qualname__ 'CDg' 

Tenga en cuenta también que en Python 3 el atributo im_class desaparece, por lo tanto, si realmente desea acceder a la clase en un decorador, necesita otro método. El enfoque que utilizo actualmente incluye el object.__set_name__ y se detalla en mi respuesta a “¿Puede un decorador de Python de un método de instancia acceder a la clase?”