web-dev-qa-db-fra.com

Comment décorer une méthode d'instance avec une classe de décorateur?

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?

23
Rafael T

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 par a.__dict__['x'], puis type(a).__dict__['x'], et continuant à travers les classes de base de type(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>.

28
thefourtheye

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)
8

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
0
PyNico