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:
-
inspect.getouterframes
yargs[0].__class__.__name__
no son adecuados para funciones simples y métodos estáticos. -
__get__
debe estar en una clase, que es rechazado por@wraps
. -
@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.