Подтвердить что ты не робот

Как я могу украсить метод экземпляра классом декоратора?

Рассмотрим этот небольшой пример:

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

который печатает

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

Почему параметр self (который должен быть экземпляром Test obj) не передан в качестве первого аргумента декорированной функции decorated?

Если я сделаю это вручную, например:

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

работает так, как ожидалось. Но если я должен знать заранее, если функция украшена или нет, она побеждает всю цель декораторов. Какая картина должна идти здесь, или я что-то не понял?

4b9b3361

Ответ 1

TL;DR

Вы можете исправить эту проблему, преобразовывая дескриптор класса Timed и возвращая частично примененную функцию из __get__, которая применяет объект Test как один из аргументов, например этот

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)

Актуальная проблема

Задание документации по Python для decorator,

Синтаксис декоратора - это просто синтаксический сахар, следующие два определения функций семантически эквивалентны:

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

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

Итак, когда вы говорите,

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

это фактически

decorated = Timed(decorated)

только объект функции передается Timed, объект, к которому он фактически привязан, не передается вместе с ним. Итак, когда вы вызываете его вот так

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

self.func будет ссылаться на несвязанный объект функции, и он вызывается с помощью Hello в качестве первого аргумента. Вот почему self печатает как Hello.


Как я могу это исправить?

Поскольку у вас нет ссылки на экземпляр Test в Timed, единственным способом сделать это будет преобразование Timed в качестве класса дескриптора. Выделив документацию, раздел "Вызов дескрипторов",

В общем, дескриптор является атрибутом объекта с "поведением привязки", тот, чей доступ к атрибуту был переопределен методами в протоколе дескриптора: __get__(), __set__() и __delete__(). Если какой-либо из этих методов определен для объекта, он называется дескриптором.

Поведение по умолчанию для доступа к атрибутам - это получение, установка или удаление атрибута из словаря объектов. Например, a.x имеет цепочку поиска, начиная с a.__dict__['x'], затем type(a).__dict__['x'] и продолжая базовые классы type(a), исключая метаклассы.

Однако , если искомое значение является объектом, определяющим один из методов дескриптора, тогда Python может переопределить поведение по умолчанию и вместо этого использовать метод дескриптора.

Мы можем сделать Timed дескриптор, просто определяя метод, подобный этому

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

Здесь self относится к самому объекту Timed, instance относится к фактическому объекту, по которому происходит поиск атрибута, и owner относится к классу, соответствующему instance.

Теперь, когда __call__ вызывается в Timed, вызывается метод __get__. Теперь, как-то, нам нужно передать первый аргумент как экземпляр класса Test (еще до Hello). Итак, мы создаем еще одну частично применимую функцию, первым параметром которой будет экземпляр Test, как этот

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

Теперь self.__call__ является связанным методом (привязанным к экземпляру Timed), а второй параметр partial является первым аргументом вызова self.__call__.

Итак, все это эффективно переводится как

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

Теперь self.decorated на самом деле Timed(decorated) (это будет теперь обозначаться как TimedObject). Всякий раз, когда мы обращаемся к нему, метод __get__, определенный в нем, вызывается и возвращает функцию partial. Вы можете подтвердить, что вот так

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

будет печатать

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

Итак,

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

переводится на

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

Так как мы возвращаем функцию partial,

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

который фактически

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

Итак, <Test obj> также становится частью *args, и когда вызывается self.func, первым аргументом будет <Test obj>.

Ответ 2

Сначала вам нужно понять как функции становятся методами и как self вводится "автоматически" .

Как только вы это знаете, "проблема" очевидна: вы украшаете функцию decorated экземпляром Timed - IOW, Test.decorated является экземпляром Timed, а не экземпляром function - и ваш класс Timed не сопоставляет реализацию типа function протокола descriptor. То, что вы хотите, выглядит так:

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)

Ответ 3

Personnaly, я использую Decorator таким образом:

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