python reflection metaprogramming

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



reflection metaprogramming (8)

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 C.f

pero en cambio aparece este mensaje de error:

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

Presumiblemente esto tiene algo que ver con el alcance de ''myFunc'' dentro de ''logger'', pero no tengo idea de qué.


En lugar de inyectar código de decoración en el momento de 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 tardía de código propio, en el momento del 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 la hora de la llamada ( __call__ ). Este mecanismo funciona tanto para métodos simples como para métodos static | class:

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


Encontré otra solución a un problema muy similar usando la biblioteca de inspect . Cuando se llama al decorador, aunque la función aún no está vinculada a la clase, puede inspeccionar la pila y descubrir qué clase llama al decorador. Al menos puede obtener el nombre de la cadena de la clase, si eso es todo lo que necesita (probablemente aún no pueda hacer referencia a él ya que se está creando). 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 demás, es la única forma en que puedo descubrir el nombre de clase del método futuro durante la llamada al decorador. Tome nota de no mantener referencias a los marcos en la documentación de la biblioteca de inspect .


La respuesta de Claudiu es correcta, pero también puedes hacer trampa al sacar el nombre de la clase del self argumento. Esto dará instrucciones de registro engañosas en casos de herencia, pero le indicará 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 haya heredado una función de una clase principal; en este caso, podrías decir

class B(C): pass b = B() b.f()

y recibe el mensaje Entering Bf donde realmente desea obtener el mensaje Entering Cf dado 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 de clase siempre deben considerarse como su primer argumento, por lo que puede 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 quise 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.


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

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

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 <bound method C.f of <__main__.C object at 0x...>>

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 y args[0].__class__.__name__ no son adecuados para funciones simples y métodos estáticos.
  2. __get__ debe estar en una clase, que es rechazado por @wraps .
  3. @wraps sí debería estar ocultando los rastros mejor.

Por lo tanto, he combinado algunas ideas de esta página, enlaces, documentos y mi propia cabeza,
y finalmente encontró una solución, que carece de las tres desventajas anteriores.

Como resultado, method_decorator :

  • Conoce la clase a la que está obligado el método decorado.
  • Oculta los rastros de decorador respondiendo a los atributos del sistema de forma más correcta que functools.wraps() .
  • Se cubre con pruebas unitarias para vincular una instancia independiente: métodos, métodos de clases, 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, e.g.: __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 función regulares. Luego solo se convierten en objetos de método no ligados. Sabiendo eso, esta es la única forma que puedo 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 C.f = logger(C.f) C().f()

Esto produce el resultado deseado.

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

C = wrapClass(C)


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