Вложенные дескрипторы/декораторы в python

Мне трудно понять, что происходит, когда я пытаюсь вложить дескрипторы/декораторы. Я использую питон 2.7.

Например, возьмем следующие упрощенные версии property и classmethod:

class MyProperty(object):
    def __init__(self, fget):
        self.fget = fget
    def __get__(self, obj, objtype=None):
        print 'IN MyProperty.__get__'
        return self.fget(obj)

class MyClassMethod(object):
    def __init__(self, f):
       self.f = f
    def __get__(self, obj, objtype=None):
        print 'IN MyClassMethod.__get__'
        def f(*args, **kwargs):
            return self.f(objtype, *args, **kwargs)
        return f

Попытка вложить их:

class A(object):
    # doesn't work:
    @MyProperty
    @MyClassMethod
    def klsproperty(cls):
        return 555
    # works:
    @MyProperty
    def prop(self):
        return 111
    # works:
    @MyClassMethod
    def klsmethod(cls, x):
        return x**2

% print A.klsproperty
IN MyProperty.__get__
...
TypeError: 'MyClassMethod' object is not callable

Метод __get__ внутреннего дескриптора MyClassMethod не вызывается. Не сумев понять, почему, я попытался добавить (как мне кажется) недействующий дескриптор:

class NoopDescriptor(object):
    def __init__(self, f):
       self.f = f
    def __get__(self, obj, objtype=None):
        print 'IN NoopDescriptor.__get__'
        return self.f.__get__(obj, objtype=objtype)

Попытка использовать дескриптор/декоратор no-op во вложении:

class B(object):
    # works:
    @NoopDescriptor
    @MyProperty
    def prop1(self):
        return 888
    # doesn't work:
    @MyProperty
    @NoopDescriptor
    def prop2(self):
        return 999

% print B().prop1
IN NoopDescriptor.__get__
IN MyProperty.__get__
888
% print B().prop2
IN MyProperty.__get__
...
TypeError: 'NoopDescriptor' object is not callable

Я не понимаю, почему B().prop1 работает, а B().prop2 нет.

Вопросы:

  1. Что я делаю не так? Почему я получаю ошибку object is not callable?
  2. Как правильно? например как лучше всего определить MyClassProperty при повторном использовании MyClassMethod и MyProperty (или classmethod и property)

person shx2    schedule 22.03.2013    source источник
comment
Ключом к интуитивному пониманию этого является то, что @MyProperty создает что-то, что работает как атрибут data, а не как метод, поэтому вы не можете вложить его в дескриптор метода. Это интуитивное понимание заводит вас далеко; полная история в ответе Иседева.   -  person abarnert    schedule 23.03.2013


Ответы (3)


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

class MyProperty(object):
    def __init__(self, fget):
        self.fget = fget
    def __get__(self, obj, objtype=None):
        print('IN MyProperty.__get__')
        try:
            return self.fget.__get__(obj, objtype)()
        except AttributeError: # self.fget has no __get__ method
            return self.fget(obj)

Теперь ваш пример кода работает:

class A(object):
    @MyProperty
    @MyClassMethod
    def klsproperty(cls):
        return 555

print(A.klsproperty)

Результат:

IN MyProperty.__get__
IN MyClassMethod.__get__
555
person Blckknght    schedule 23.03.2013
comment
Спасибо, это имеет смысл. Но является ли это рекомендуемым подходом? Если да, то почему встроенные property и classmethod не ведут себя таким образом (и не могут быть вложенными)? - person shx2; 23.03.2013
comment
Я не уверен, что это обескураживает или почему встроенный property еще не делает этого. Я знаю, что есть некоторые ограничения, которые это не устраняет. Во-первых, вы не можете вкладывать дескрипторы в обратном порядке, даже если вы сделали MyClassMethod более умным. И я не думаю, что функция дескриптора __set__ вызывается для назначения переменной класса, поэтому вам нужно проделать некоторую магию метакласса, чтобы записываемые свойства класса работали. Возможно, кто-то с большим опытом дескрипторного программирования сможет оценить другие проблемы и ограничения. - person Blckknght; 24.03.2013

В этом случае, когда декораторы используются без параметров, декоратор вызывается с функцией, которую он украшает, в качестве параметра. Вместо декоративной функции используется возвращаемое декоратором значение. Так:

@MyProperty
def prop(self):
    ...

эквивалентно:

def prop(self):
    ...
prop = MyProperty(prop)

Поскольку MyProperty реализует протокол дескриптора, доступ к A.prop фактически вызовет A.prop.__get__(), а вы определили __get__ для вызова декорированного объекта (в данном случае исходной функции/метода), так что все работает нормально.

Теперь во вложенном случае:

@MyProperty
@MyClassMethod
def prop(self):
    ...

Эквивалент:

def prop(self):
    ...
prop = MyClassMethod(prop)   # prop is now instance of MyClassMethod
prop = MyProperty(prop)      # prop is now instance of MyProperty
                             # (with fget == MyClassMethod instance)

Теперь, как и раньше, при доступе к A.prop будет фактически вызываться A.prop.__get__()MyProperty), который затем пытается вызвать экземпляр MyClassMethod (объект, который был декорирован и сохранен в атрибуте fget).

Но для MyClassMethod не определен метод __call__, поэтому вы получаете ошибку MyClassMethod is not callable.


И чтобы ответить на ваш второй вопрос: свойство уже является атрибутом класса - в вашем примере доступ к A.prop вернет значение свойства в объекте класса, а A().prop вернет значение свойства в объекте экземпляра (который может быть такой же, как объект класса, если экземпляр не переопределял его).

person isedev    schedule 22.03.2013
comment
Ваше объяснение, если ясно. Спасибо. Что касается второго ответа: я не уверен, что понимаю, что вы имеете в виду. Я ищу что-то параллельное classmethod, что означает, что я могу назвать это либо A.prop, либо A().prop, и в обоих случаях базовой функции передается аргумент cls. - person shx2; 23.03.2013
comment
В этом случае вы можете проверить тип аргумента obj: если это экземпляр класса, вы можете извлечь из него класс и передать его декорированной функции вместо объекта экземпляра. - person isedev; 23.03.2013
comment
Вы упускаете суть... Мой вопрос не о том, как реализовать декоратор classproperty. Речь идет о вложенных дескрипторах. Предложенная вами логика уже реализована в classmethod. Я ожидаю, что должен быть чистый способ повторного использования/объединения property и classmethod для достижения этого вместо повторной реализации логики в них. Но я начинаю думать, что нет... - person shx2; 23.03.2013
comment
Но они НЕ применяются к одному и тому же: property применяется к дескрипторам, а classmethod применяется к вызываемому (методу) — два разных протокола. Как вы предполагаете их комбинировать? - person isedev; 23.03.2013
comment
Так что я, вероятно, не понимаю, почему свойство применяется к дескрипторам. Вы можете сделать foo = property(foo), где foo действует как fget, а foo вызывается и будет вызываться при доступе к self.foo. Я думал, что это означает, что свойство применяется к вызываемым объектам, как и classmethod. - person shx2; 23.03.2013
comment
Подумайте об этом так: вы обращаетесь к свойству как к переменной A.prop = value, но вызываете метод A.method(). Несмотря на то, что дескриптор можно вызывать, вы не передаете ему произвольные аргументы (только объект), а передаете произвольные аргументы методу (зависит от сигнатуры метода). - person isedev; 23.03.2013

Я нашел окончательный ответ на свой старый вопрос в увлекательной книге Грэма Дамплтона блог.

Короче говоря, декораторы, которые я написал, не соблюдают протокол дескрипторов, пытаясь вызвать обернутую функцию/объект напрямую, вместо того, чтобы сначала дать им возможность выполнить свою «магию дескриптора» (путем вызова их __get__() сначала).

person shx2    schedule 01.08.2014