una metodo llamar listas instanciar instancia ejemplo crear con como clases clase python class self python-decorators

python - metodo - ¿Cómo puedo decorar un método de instancia con una clase de decorador?



python ejemplo clases (3)

Primero debe comprender cómo las funciones se convierten en métodos y cómo se inyecta "automágicamente" a uno mismo .

Una vez que sabe eso, el "problema" es obvio: está decorando la función decorated con una instancia Timed - Test.decorated , Test.decorated es una instancia Timed , no una instancia function - y su clase Timed no imita la implementación del tipo de function de El protocolo descriptor . Lo que quieres se ve así:

import types class Timed(object): def __init__(self, f): self.func = f def __call__(self, *args, **kwargs): start = dt.datetime.now() ret = self.func(*args, **kwargs) time = dt.datetime.now() - start ret["time"] = time return ret def __get__(self, instance, cls): return types.MethodType(self, instance, cls)

Considere este pequeño ejemplo:

import datetime as dt class Timed(object): def __init__(self, f): self.func = f def __call__(self, *args, **kwargs): start = dt.datetime.now() ret = self.func(*args, **kwargs) time = dt.datetime.now() - start ret["time"] = time return ret class Test(object): def __init__(self): super(Test, self).__init__() @Timed def decorated(self, *args, **kwargs): print(self) print(args) print(kwargs) return dict() def call_deco(self): self.decorated("Hello", world="World") if __name__ == "__main__": t = Test() ret = t.call_deco()

que imprime

Hello () {''world'': ''World''}

¿Por qué el parámetro self (que debería ser la instancia de prueba obj) no se pasa como primer argumento a la función decorated ?

Si lo hago manualmente, como:

def call_deco(self): self.decorated(self, "Hello", world="World")

Funciona como se esperaba. Pero si debo saber de antemano si una función está decorada o no, anula todo el propósito de los decoradores. ¿Cuál es el patrón para ir aquí, o no he entendido algo?


Yo uso decoradores de la siguiente manera:

def timeit(method): def timed(*args, **kw): ts = time.time() result = method(*args, **kw) te = time.time() ts = round(ts * 1000) te = round(te * 1000) print(''%r (%r, %r) %2.2f millisec'' % (method.__name__, args, kw, te - ts)) return result return timed class whatever(object): @timeit def myfunction(self): do something


tl; dr

Puede solucionar este problema haciendo que la clase Timed un descriptor y devuelva una función parcialmente aplicada de __get__ que aplica el objeto Test como uno de los argumentos, como este

class Timed(object): def __init__(self, f): self.func = f def __call__(self, *args, **kwargs): print(self) start = dt.datetime.now() ret = self.func(*args, **kwargs) time = dt.datetime.now() - start ret["time"] = time return ret def __get__(self, instance, owner): from functools import partial return partial(self.__call__, instance)

El problema real

Citando la documentación de Python para decorator ,

La sintaxis del decorador es simplemente azúcar sintáctica, las siguientes dos definiciones de funciones son semánticamente equivalentes:

def f(...): ... f = staticmethod(f) @staticmethod def f(...): ...

Entonces, cuando dices,

@Timed def decorated(self, *args, **kwargs):

en realidad es

decorated = Timed(decorated)

solo el objeto de función se pasa al Timed , el objeto al que está realmente vinculado no se pasa junto con él . Entonces, cuando lo invocas así

ret = self.func(*args, **kwargs)

self.func se referirá al objeto de función self.func y se invoca con Hello como primer argumento. Es por eso que self imprime como Hello .

¿Cómo puedo arreglar esto?

Dado que no tiene referencia a la instancia de Test en Timed , la única forma de hacerlo sería convertir Timed como una clase de descriptor . Citando la documentación, invocando la sección de descriptores ,

En general, un descriptor es un atributo de objeto con "comportamiento de enlace", uno cuyo acceso al atributo ha sido anulado por los métodos del protocolo del descriptor: __get__() , __set__() y __delete__() . Si alguno de esos métodos se define para un objeto, se dice que es un descriptor.

El comportamiento predeterminado para el acceso al atributo es obtener, establecer o eliminar el atributo del diccionario de un objeto. Por ejemplo, ax tiene una cadena de búsqueda que comienza con a.__dict__[''x''] , luego type(a).__dict__[''x''] y continúa a través de las clases base del type(a) excluyendo las metaclases.

Sin embargo, si el valor buscado es un objeto que define uno de los métodos del descriptor, Python puede anular el comportamiento predeterminado e invocar el método del descriptor .

Podemos hacer de Timed un descriptor, simplemente definiendo un método como este

def __get__(self, instance, owner): ...

Aquí, self refiere al objeto Timed mismo, instance refiere al objeto real en el que está ocurriendo la búsqueda de atributos y el owner refiere a la clase correspondiente a la instance .

Ahora, cuando se invoca __call__ en Timed , se __get__ método __get__ . Ahora, de alguna manera, necesitamos pasar el primer argumento como la instancia de la clase Test (incluso antes de Hello ). Entonces, creamos otra función parcialmente aplicada, cuyo primer parámetro será la instancia de Test , como esta

def __get__(self, instance, owner): from functools import partial return partial(self.__call__, instance)

Ahora, self.__call__ es un método enlazado (enlazado a una instancia Timed ) y el segundo parámetro a partial es el primer argumento para la llamada self.__call__ .

Entonces, todo esto efectivamente se traduce así

t.call_deco() self.decorated("Hello", world="World")

Ahora self.decorated es en realidad un objeto Timed(decorated) (esto se denominará TimedObject de ahora en adelante). Cada vez que accedemos a él, se __get__ método __get__ definido en él y devolverá una función partial . Puedes confirmar eso así

def call_deco(self): print(self.decorated) self.decorated("Hello", world="World")

imprimiría

<functools.partial object at 0x7fecbc59ad60> ...

Asi que,

self.decorated("Hello", world="World")

se traduce a

Timed.__get__(TimedObject, <Test obj>, Test.__class__)("Hello", world="World")

Como devolvemos una función partial ,

partial(TimedObject.__call__, <Test obj>)("Hello", world="World"))

que es en realidad

TimedObject.__call__(<Test obj>, ''Hello'', world="World")

Entonces, <Test obj> también se convierte en parte de *args , y cuando se invoca self.func , el primer argumento será <Test obj> .