2008-11-20 14 views
54

estoy tratando de escribir un decorador para hacer conexiones:Python decorador hace la función hay que olvidar que pertenece a una clase

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 de impresión:

Entering C.f 

pero en cambio me sale este mensaje de error:

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

es de suponer que esto es algo que ver con el alcance de 'myFunc' interior 'registrador', pero no tengo i dea qué.

+0

No es exactamente una respuesta, pero encontró este artículo para cubrir las cosas en profundidad http://bit.ly/1NsBLmx – apcelent

Respuesta

41

La respuesta de Claudiu es correcta, pero también puede hacer trampa obteniendo el nombre de clase del argumento self. 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 ya he dicho, esto no funcionará correctamente en los casos en que haya heredado una función a partir de una clase padre; en este caso se podría decir

class B(C): 
    pass 

b = B() 
b.f() 

y obtener el mensaje Entering B.f donde realmente se quiere hacer llegar el mensaje Entering C.f ya que es la clase correcta. Por otro lado, esto podría ser aceptable, en cuyo caso recomendaría este enfoque sobre la sugerencia de Claudiu.

+1

error tipográfico: olvidó 'return with_logging' en la función de registro. –

+2

por cierto, functools.wraps no conserva los atributos im_ *. ¿Crees que esta omisión podría considerarse un error? –

+1

No puedo pretender que entiendo completamente lo que está pasando con las @wraps todavía, pero definitivamente soluciona mi problema. Muchas gracias. –

7

Parece que mientras se crea la clase, Python crea objetos de funciones regulares. Luego solo se convierten en objetos de método no ligados. Sabiendo eso, esta es la única manera que pude encontrar para hacer lo que quiere:

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 envolver todos los métodos en una clase, entonces es probable que desee crear una función wrapClass, que luego se podría utilizar como esto:

C = wrapClass(C) 
+0

wrapclass debe tener cuidado debido al método estático. –

+0

Esto parece un buen caso de uso para decoradores de clase (nuevo en Python 2.6). Funcionan exactamente de la misma manera que los decoradores de funciones. – babbageclunk

6

funciones de clase siempre deben tomar uno mismo 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() 

en un primer momento que quería utilizar self.__name__ pero eso no funciona porque la instancia no tiene nombre. debe usar self.__class__.__name__ para obtener el nombre de la clase.

0

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

25

Las funciones solo se convierten en métodos en tiempo de ejecución. Es decir, cuando obtiene C.f obtiene una función vinculada (y C.f.im_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 los descriptores para lograr esto de una manera más general.Este patrón se describe in a blog post on Decorators and Descriptors y una implementación de su decorador registrador 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 (usando, por ejemplo, getattr(self.func, 'im_class', None)), pero este patrón general funcionará para ambos métodos y funciones. Sin embargo, no se trabajo para las clases de estilo antiguo (pero simplemente no utilizar aquellos;)

+0

Para cualquier persona que busque una función que ofrezca 'TypeError: foo toma exactamente x argumentos' o 'atributo perdido' y se dio cuenta de que el argumento 'self' s no se pasa a su función decorada, esta es la solución, Gracias @ianb – theannouncer

+0

Este enfoque requiere un registrador diferente para método vinculado, método independiente y función explotación florestal. – KeatsKelleher

6

he encontrado 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 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 otros, que es la forma única puedo averiguar para descubrir el nombre de la clase del futuro método durante la llamada al decorador. Tome nota de no mantener referencias a los marcos en la documentación de la biblioteca inspect.

+2

esto es exactamente lo que quiero: información sobre el método y la clase a la que estará vinculado _antes de que se llame la primera vez. – 203

16

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

  1. inspect.getouterframes y args[0].__class__.__name__ no son adecuados para las funciones de desnudos y de los métodos estáticos.
  2. __get__ debe estar en una clase, que se rechaza por @wraps.
  3. @wraps en sí debería estar ocultando los rastros mejor.

lo tanto, me 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 desventajas anteriormente.

Como resultado, method_decorator:

  • conoce la clase el método decorado está enlazado.
  • Oculta los rastros de decorador respondiendo a los atributos del sistema más correctamente que functools.wraps().
  • Está cubierto con pruebas unitarias para vincular un método de instancia independiente, 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 full unit-tests for usage details.

Y aquí es sólo 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__() 
+0

Este fue el único enfoque que funcionó para mí. Necesito la referencia de instancia de objeto en su lugar – Simon

0

En lugar de inyectar código de decoración a la hora de la definición, cuando la función no sabe que es la clase, se accede retraso de ejecutar este código hasta que la función/llamada .objeto descriptor facilita la inserción de código propio tarde, 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 clase tanto en el tiempo de acceso (__get__) y al momento de llamar (__call__). Este mecanismo funciona con los métodos de fricción estática, así como los métodos 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

Cuestiones relacionadas