Considérez ce petit exemple:
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 __== "__main__":
t = Test()
ret = t.call_deco()
qui imprime
Hello
()
{'world': 'World'}
Pourquoi le paramètre self
(qui devrait être l'instance de test obj) n'est-il pas passé en tant que premier argument de la fonction décorée decorated
?
Si je le fais manuellement, comme:
def call_deco(self):
self.decorated(self, "Hello", world="World")
cela fonctionne comme prévu. Mais si je dois savoir à l'avance si une fonction est décorée ou non, cela irait à l'encontre de l'objectif des décorateurs. Quelle est la tendance à aller ici, ou ai-je mal compris quelque chose?
tl; dr
Vous pouvez résoudre ce problème en convertissant la classe Timed
en descripteur et en renvoyant une fonction partiellement appliquée à partir de __get__
qui applique l'objet Test
comme l'un des arguments, comme celui-ci.
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)
Le problème actuel
Citant la documentation Python pour décorateur ,
La syntaxe du décorateur est simplement du sucre syntaxique, les deux définitions de fonction suivantes sont sémantiquement équivalentes:
def f(...): ... f = staticmethod(f) @staticmethod def f(...): ...
Alors, quand vous dites,
@Timed
def decorated(self, *args, **kwargs):
c'est en fait
decorated = Timed(decorated)
seul l'objet fonction est transmis à la Timed
, l'objet auquel il est lié n'est pas transmis en même temps . Alors, quand vous l'invoquez comme ça
ret = self.func(*args, **kwargs)
self.func
fera référence à l'objet fonction non lié et il est appelé avec Hello
comme premier argument. C'est pourquoi self
s'imprime sous la forme Hello
.
Comment puis-je résoudre ce problème?
Etant donné que vous n'avez aucune référence à l'instance Test
dans la Timed
, la seule façon de procéder consiste à convertir Timed
en tant que classe de descripteur. Citation de la documentation, section Invoking descriptors ,
En général, un descripteur est un attribut d'objet ayant un «comportement de liaison», celui dont l'accès aux attributs a été remplacé par des méthodes du protocole de descripteur:
__get__()
,__set__()
et__delete__()
. Si l'une de ces méthodes est définie pour un objet, on dit qu'il s'agit d'un descripteur.Le comportement par défaut pour l’accès aux attributs consiste à obtenir, définir ou supprimer l’attribut du dictionnaire de l’objet. Par exemple,
a.x
a une chaîne de recherche commençant para.__dict__['x']
, puistype(a).__dict__['x']
, et continuant à travers les classes de base detype(a)
, à l'exclusion des métaclasses.Cependant, si la valeur recherchée est un objet définissant l'une des méthodes de descripteur, Python peut alors remplacer le comportement par défaut et appeler la méthode de descripteur à la place .
On peut faire Timed
un descripteur, en définissant simplement une méthode comme celle-ci
def __get__(self, instance, owner):
...
Ici, self
fait référence à l'objet Timed
lui-même, instance
à l'objet réel sur lequel la recherche d'attribut est en cours et owner
à la classe correspondant à instance
.
Désormais, lorsque __call__
est appelé sur Timed
, la méthode __get__
sera invoquée. Maintenant, d’une manière ou d’une autre, nous devons passer le premier argument en tant qu’instance de la classe Test
(même avant la Hello
). Nous créons donc une autre fonction partiellement appliquée, dont le premier paramètre sera l’instance Test
, comme ceci
def __get__(self, instance, owner):
from functools import partial
return partial(self.__call__, instance)
Désormais, self.__call__
est une méthode liée (liée à une instance Timed
) et le second paramètre à partial
est le premier argument de l'appel self.__call__
.
Donc, tout cela se traduit efficacement comme ceci
t.call_deco()
self.decorated("Hello", world="World")
Maintenant, self.decorated
est en réalité l'objet Timed(decorated)
(à partir de maintenant, ce sera TimedObject
). Chaque fois que nous y accéderons, la méthode __get__
définie dans celle-ci sera invoquée et renvoie une fonction partial
. Vous pouvez confirmer que comme ça
def call_deco(self):
print self.decorated
self.decorated("Hello", world="World")
serait imprimer
<functools.partial object at 0x7fecbc59ad60>
...
Alors,
self.decorated("Hello", world="World")
est traduit en
Timed.__get__(TimedObject, <Test obj>, Test.__class__)("Hello", world="World")
Puisque nous retournons une fonction partial
,
partial(TimedObject.__call__, <Test obj>)("Hello", world="World"))
qui est en fait
TimedObject.__call__(<Test obj>, 'Hello', world="World")
Ainsi, <Test obj>
devient également une partie de *args
, et lorsque self.func
est appelé, le premier argument sera le <Test obj>
.
Vous devez d’abord comprendre comment les fonctions deviennent des méthodes et comment self
est injecté "automatiquement" .
Une fois que vous savez cela, le "problème" est évident: vous décorez la fonction decorated
avec une instance Timed
- IOW, Test.decorated
est une instance Timed
, pas une instance function
- et votre classe Timed
ne reproduit pas l'implémentation du type function
par descriptor
protocole. Ce que vous voulez ressemble à ceci:
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)
Personnellement, j'utilise Decorator de cette façon:
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