Вызываемый декоратор объекта, примененный к методу, не получает аргумент на входе

import functools


class Decor(object):
    def __init__(self, func):
         self.func = func

    def __call__(self, *args, **kwargs):
        def closure(*args, **kwargs):
            print args, kwargs
            return self.func(*args, **kwargs)
        return closure(*args, **kwargs)


class Victim(object):
    @Decor
    def sum(self, a, b):
        return a+b


v = Victim()
v.sum(1, 2)

Результат:

(1, 2) {}
Traceback (most recent call last):
  File "test.py", line 19, in <module>
    v.sum(1, 2)
  File "test.py", line 11, in __call__
    return closure(*args, **kwargs)
  File "test.py", line 10, in closure
    return self.func(*args, **kwargs)
TypeError: sum() takes exactly 3 arguments (2 given)

Как получить аргумент self для метода?

ОБНОВЛЕНИЕ: мне удалось создать более полезную адаптацию ответа Мартейна, которая возвращает объект Decor в ответ на __get__, но в то же время связывает аргумент self, когда он вызывается как метод объекта . С этой версией вы можете сказать, например. Victim.sum.hooks.append(my_favorite_function) и my_favorite_function будут вызываться перед Victim.sum. ВНИМАНИЕ: эта версия небезопасна для потоков.

class Decor(object):
    def __init__(self, func):
        self.func = func
        self.hooks = []
        wraps(self.func)(self)

    def __get__(self, instance, klass):
        if instance != None: self.instance = instance
        if klass != None: self.klass = klass
        return self

    def __call__(self, *args, **kwargs):
        def closure(*args, **kwargs):
           for function in self.hooks:
               function(*args, **kwargs)
           func = self.func
           retval = func(*args, **kwargs) #kwargs_copy #called with notify = False
           return retval
        return closure.__get__(self.instance, self.klass)(*args, **kwargs)

person Boris Burkov    schedule 20.03.2014    source источник
comment
Простой способ: пусть декоратор возвращает функцию вместо экземпляра класса. Менее простой способ: реализовать протокол дескриптора, как настоящую функцию.   -  person user2357112 supports Monica    schedule 21.03.2014
comment
Вы назвали свою вложенную функцию __closure__, но затем вызвали closure(). Это опечатка?   -  person Martijn Pieters    schedule 21.03.2014
comment
О, простите, это опечатка. Я исправил это в консоли раньше во время тестирования, но забыл исправить в вопросе.   -  person Boris Burkov    schedule 21.03.2014


Ответы (1)


Функции Python действуют как дескрипторы, что означает, что всякий раз, когда вы обращаетесь к функции в классе или экземпляре, их < вызывается метод href="http://docs.python.org/2/reference/datamodel.html#object.__get__">.__get__(), и возвращается объект метода, который сохраняет ссылку на исходную функцию, и для экземпляров ссылка на экземпляр. Затем объект метода действует как обертка; при вызове они вызывают базовую функцию и передают ссылку на экземпляр как self.

Ваш вызываемый объект класса, с другой стороны, не реализует протокол дескриптора, у него нет метода .__get__(), и, следовательно, у него никогда не будет возможности привязаться к экземпляру. Вам придется реализовать эту функциональность самостоятельно:

class Decor(object):
    def __init__(self, func):
         self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            return self
        d = self
        # use a lambda to produce a bound method
        mfactory = lambda self, *args, **kw: d(self, *args, **kw)
        mfactory.__name__ = self.func.__name__
        return mfactory.__get__(instance, owner)

    def __call__(self, instance, *args, **kwargs):
        def closure(*args, **kwargs):
            print instance, args, kwargs
            return self.func(instance, *args, **kwargs)
        return closure(*args, **kwargs)

Демо:

>>> class Victim(object):
...     @Decor
...     def sum(self, a, b):
...         return a+b
... 
>>> v = Victim()
>>> v.sum
<bound method Victim.sum of <__main__.Victim object at 0x11013d850>>
>>> v.sum(1, 2)
<__main__.Victim object at 0x11013d850> (1, 2) {}
3

Не не рекомендуется хранить экземпляр, к которому вы привязаны, непосредственно в экземпляре Decor; это атрибут класса, общий для экземпляров. Параметр self.instance не является потокобезопасным и не позволяет сохранять методы для последующего вызова; самый последний вызов __get__ изменит self.instance и приведет к трудноразрешимым ошибкам.

Вы всегда можете вернуть собственный прокси-объект вместо метода:

class DecorMethod(object):
    def __init__(self, decor, instance):
        self.decor = decor
        self.instance = instance

    def __call__(self, *args, **kw):
        return self.decor(instance, *args, **kw)

    def __getattr__(self, name):
        return getattr(self.decor, name)

    def __repr__(self):
        return '<bound method {} of {}>'.format(self.decor, type(self))

и используйте это в своем Decor.__get__ вместо создания метода:

def __get__(self, instance, owner):
    if instance is None:
        return self
    return DecorMethod(self, instance)

Здесь DecorMethod передает любые запросы неизвестных атрибутов обратно экземпляру декоратора Decor:

>>> class Victim(object):
...     @Decor
...     def sum(self, a, b):
...         return a + b
... 
>>> v = Victim()
>>> v.sum
<bound method <__main__.Decor object at 0x102295390> of <class '__main__.DecorMethod'>>
>>> v.sum.func
<function sum at 0x102291848>
person Martijn Pieters    schedule 21.03.2014
comment
Спасибо за четкое объяснение. Я наткнулся на другой пример в библиотеке декораторов Python, прежде чем узнал, что вы опубликовали свой ответ: wiki .python.org/moin/ - person Boris Burkov; 21.03.2014
comment
Мартин, не могли бы вы предложить способ адаптировать ваше решение для следующей цели? Я хотел бы сохранить список функций ловушек в Decor и Decor.__call__ вызывать каждую из них перед вызовом декорированной функции (например, Victim.sum). Мне нужно будет обновить список хуков во время выполнения, сказав Victim.sum.hooks.append(another_func). В то же время мне нужно будет иметь возможность привязать аргумент self к украшенной функции (например, Victim.sum должен получить экземпляр Victim в качестве аргумента self). Можно ли достичь обеих этих способностей одновременно? - person Boris Burkov; 22.03.2014
comment
@Bob: Это довольно просто; Я сделал объект instance, передаваемый в метод __call__ вашего объекта Decor, более явным; вы можете добавить туда любое количество дополнительных функций, которым необходимо передавать instance в них, но вам нужно передать их в декорированную функцию (self.func). - person Martijn Pieters; 22.03.2014
comment
Спасибо за ответ, но так, как вы предложили, вы не можете сказать, что v.sum.hooks.append(another_function): v.sum - это связанный метод, сгенерированный из mfactory, а не Decor вызываемого объекта. Не дает доступа к атрибутам Декора. На самом деле, когда я делал Decor класс, а не функцию, моей целью было хранить хуки в его атрибутах. Я хотел, чтобы __get__ вернул сам экземпляр Decor, и кажется, что моя адаптация вашего ответа (обновленный вопрос с ним) работает. Спасибо! - person Boris Burkov; 22.03.2014
comment
Вы можете получить доступ к декоратору класса (методы __get__ в особых случаях). - person Martijn Pieters; 22.03.2014
comment
@Bob: у вашего подхода есть серьезные недостатки; вы напрямую изменяете экземпляр декоратора, что означает, что а) вы сделали код небезопасным для потоков и б) если вы сохранили два таких «метода» в ссылках, выигрывает самая последняя привязка. Это приведет к очень странным ошибкам. - person Martijn Pieters; 22.03.2014
comment
@Bob: В моем подходе повторно использовался существующий прокси-объект MethodType (с помощью ярлыка lambda), вы также можете вернуть свой собственный прокси-объект, который выполняет ту же роль, и позволяет добавлять дополнительные функции. Поскольку вы создаете отдельные экземпляры для моделирования привязки (как метод), вы сохраняете это потокобезопасным и безопасным для хранения ссылок для последующего вызова. - person Martijn Pieters; 22.03.2014
comment
@Bob: я расширил ответ, чтобы показать вам, как вместо этого написать собственный прокси-сервер метода. - person Martijn Pieters; 22.03.2014
comment
Действительно, моя версия небезопасна для потоков. С прокси-подходом вы не можете сказать v.sum.hooks.append(function), но вы можете получить доступ к объекту Decorator из class, а не из object. Спасибо за помощь, Мартейн. У меня есть ощущение, что возня с чрезмерно сложными декораторами может сделать мой проект хрупким и недружественным для пользователей. По крайней мере, я не буду хранить хуки в самом декораторе и перенесу их в атрибуты экземпляра. - person Boris Burkov; 24.03.2014
comment
@Bob: с DecorMethod вы можете использовать v.sum.hooks.append(), поскольку атрибут hooks проксируется. - person Martijn Pieters; 24.03.2014
comment
А, getattr служит этой цели. Понятно. Ваше здоровье! - person Boris Burkov; 24.03.2014