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 cona.__dict__[''x'']
, luegotype(a).__dict__[''x'']
y continúa a través de las clases base deltype(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>
.