Попытка запрограммировать общий декоратор to_class

Предположим, я определил:

def to_class(cls):
  """ returns a decorator
  aimed to force the result to be of class cls. """
  def decorating_func(func):
    def wrapper(*args, **kwargs):
      return cls(func(*args, **kwargs))
    return wrapper
  return decorator(decorating_func)

Я хочу использовать его для создания декораторов, которые превращают результаты функций в объекты данного класса. Однако это не сработает:

class TestClass(object):
  def __init__(self, value):
    self._value = (value, value)

  def __str__(self):
    return str(self._value)

  @staticmethod
  @to_test_class
  def test_func(value):
    return value

to_test_class = to_class(TestClass)

поскольку test_func будет искать to_test_class и не найдет его. С другой стороны, размещение присваивания to_test_class перед определением класса также не удастся, так как TestClass еще не будет определен.

Попытка поместить @to_class(TestClass) выше определения test_func также не удастся, поскольку метод создается до класса (если я не ошибаюсь).

Единственный обходной путь, который я нашел, - это определить to_test_class вручную как декоратор, а не как возвращаемый из общего определения "to_class".

Возможно, важно упомянуть, что это только базовый пример, но я хочу использовать to_class для многих приложений, таких как изменение возвращаемого значения перед «подключением» его в конструктор класса; и я хочу использовать его как декоратор для методов других классов.

Я уверен, что некоторые считают декоратор to_class бессмысленным; Вместо этого манипуляции могут быть выполнены внутри декорированного метода. Тем не менее, я нахожу это удобным, и это помогает мне с читабельностью.

Наконец, я хочу добавить, что это интересует меня на 20% из практических соображений и на 80% из соображений изучения, так как я не совсем понимаю декораторы в Python в целом.


person Bach    schedule 16.11.2012    source источник


Ответы (3)


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

Я могу придумать один обходной путь: не использовать декоратор staticmethod. Вместо этого внутри вашего собственного декоратора повторно используйте декоратор classmethod. Таким образом, вы гарантируете, что Python, по крайней мере, пройдет для вас связанный класс:

def to_class(func):
    """ returns a decorator
    aimed to force the result to be of class cls. """
    def wrapper(cls, *args, **kwargs):
        return cls(func(*args, **kwargs))
    return classmethod(wrapper)

Затем используйте его следующим образом:

class TestClass(object):
    def __init__(self, value):
        self._value = (value, value)

    def __str__(self):
        return str(self._value)

    @to_class
    def test_func(value):
        return value

Демонстрация:

>>> def to_class(func):
...     """ returns a decorator
...     aimed to force the result to be of class cls. """
...     def wrapper(cls, *args, **kwargs):
...         return cls(func(*args, **kwargs))
...     return classmethod(wrapper)
... 
>>> class TestClass(object):
...     def __init__(self, value):
...         self._value = (value, value)
...     def __str__(self):
...         return str(self._value)
...     @to_class
...     def test_func(value):
...         return value
... 
>>> TestClass.test_func('foo')
<__main__.TestClass object at 0x102a77210>
>>> print TestClass.test_func('foo')
('foo', 'foo')

Обобщенная версия вашего декоратора непроста; единственный другой способ обойти вашу головоломку — использовать хак метакласса; см. другой мой ответ где я описываю метод более подробно.

По сути, вам нужно обратиться к пространству имен class-in-constructing, установить временный метакласс, а затем полагаться на наличие хотя бы одного экземпляра класса, прежде чем ваш декоратор заработает; подход временного метакласса подключается к механизмам создания класса, чтобы позже получить сконструированный класс.

Однако, учитывая, что вы используете этот декоратор в качестве альтернативной фабрики классов, это, вероятно, не будет идеальным; если бы кто-то использовал ваши декорированные функции для создания исключительно экземпляров класса, метакласс был бы вызван слишком поздно.

person Martijn Pieters    schedule 16.11.2012
comment
Не будет работать для обычных методов (они не могут быть преобразованы в методы классов), но, конечно, есть аналогичный подход, использующий для них .__class__. Хорошее решение! - person Alfe; 16.11.2012
comment
@Alfe: «обычные» методы уже являются частью экземпляра, поэтому нет необходимости возвращать экземпляр класса. - person Martijn Pieters; 16.11.2012
comment
Мы говорим об обертывании их возвращаемого значения. «Обычные» методы не обязательно возвращают экземпляр класса, в котором они находятся. Но я думаю, что только ваша формулировка меня здесь смутила. Возможно, вы имели в виду, что «не нужно передавать им класс» (и это то, что я имел в виду, когда упомянул .__class__). - person Alfe; 16.11.2012
comment
@Alfe: конечно, если бы вы хотели использовать этот декоратор в «нормальном» методе, вам нужно было бы избегать декоратора classmethod и получать класс из первого параметра (который является экземпляром, лично я бы использовал type(args[0]) в этом случае ). Я на 99,9% уверен, что ОП не имел в виду этот сценарий. :-) - person Martijn Pieters; 16.11.2012
comment
Однако его определение to_class довольно общее (например, передача в него произвольного класса); и поскольку это доска, а не сессия PM, другие люди могут прочитать это позже, поэтому важно не только намерение спрашивающего; также то, что другие люди могли сделать из его вопроса, и я, по крайней мере, понял его по-другому, так что ваша вероятность 99,9% не основана на точных цифрах (у меня пока более 0,1% читателей). ;-) - person Alfe; 16.11.2012
comment
Мне нравится использование type(x) вместо x.__class__. .__class__ явно упоминает, что я ожидаю экземпляр и хочу его класс, но использование type() не выглядит таким уродливым со всеми символами подчеркивания, которые делают его похожим на копание в грязи внутренних компонентов компилятора. - person Alfe; 16.11.2012
comment
@Alfe: Достаточно справедливое замечание, но первоначальный вопрос ОП гораздо больше касается отсутствия доступа к классу, пока он все равно создается. Я могу предоставить более изощренные методы для обхода этого ограничения, но для варианта использования, продемонстрированного в вопросе, достаточно использовать декоратор classmethod. - person Martijn Pieters; 16.11.2012
comment
Это правда. Я просто думаю, что он имел в виду общий декоратор (возможно, даже использовал его в нескольких местах вне классов) и столкнулся с проблемами, используя его в этом случае. В конце концов, использование принуждения результата к определенному классу никоим образом не ограничивается статическими методами. - person Alfe; 16.11.2012
comment
Я хочу иметь возможность использовать to_test_class во многих местах; в определении TestClass (где он может быть определен иначе, скажем, с помощью декорированного метода to_self), в определении других классов или даже в неограниченных методах. - person Bach; 16.11.2012
comment
Ха! Победа 0,1%! Посмотрите на обновление моего ответа, но я предупреждаю вас: это все равно будет некрасиво. - person Alfe; 16.11.2012
comment
@HansZauber: Обновил мой ответ немного более подробно, боюсь, в этом случае вы все еще застряли с худшим обходным путем. - person Martijn Pieters; 16.11.2012

Ну, вы забыли, что class — это первый параметр, передаваемый методу, украшенному classmethod, поэтому вы можете написать это так:

def to_this_class(func):
    def wrapped(cls, value):
        res = func(cls, value)
        return cls(res)
    return wrapped

class TestClass(object):
    def __init__(self, value):
        self._value = (value, value)

    def __str__(self):
        return str(self._value)

    @classmethod
    @to_this_class
    def test_func(cls, value):
        return value

x = TestClass('a')

print x.test_func('b')
person Bunyk    schedule 16.11.2012
comment
Однако требует добавления «cls». И это невозможно, если требуется другой класс (в конце концов, мы могли бы захотеть вернуть экземпляр класса, отличного от класса, который мы сейчас определяем). - person Alfe; 16.11.2012
comment
@Alfe, да, именно поэтому я назвал это to_this_class. Для других классов он мог бы использовать свой декоратор, если бы не было проблем с круговыми зависимостями. - person Bunyk; 16.11.2012

Проблема в том, что декоратор оценивается при определении того, что он украшает, поэтому при определении метода test_func() вызывается декоратор to_test_class, и даже если он уже существует, объект, над которым он должен работать (т. класс TestClass) еще не существует (поскольку он создается после создания всех методов).

Возможно, вы можете использовать заполнитель в точке, где используется класс, а позже (после создания класса) заполнить это значение (класс) в точке заполнителя.

Пример:

lazyClasses = {}
def to_lazy_class(className):
  """ returns a decorator
  aimed to force the result to be of class cls. """
  def decorating_func(func):
    def wrapper(*args, **kwargs):
      return lazyClasses[className](func(*args, **kwargs))
    return wrapper
  return decorating_func 

class TestClass(object):
  def __init__(self, value):
    self._value = (value, value)

  def __str__(self):
    return str(self._value)

  @staticmethod
  @to_lazy_class('TestClass')
  def test_func(value):
    return value

lazyClasses['TestClass'] = TestClass

>>> TestClass.test_func('hallo')
<__main__.TestClass object at 0x7f76d8cba190>
person Alfe    schedule 16.11.2012
comment
Да. Вы каким-то образом должны ввести значение в декоратор позже, потому что оно еще не существует, когда вы вызываете декоратор. Фактическое использование значения происходит не при вызове декоратора, а при последующем вызове декорированного метода, поэтому внедрение работает лениво. Но даже если бы вы могли ограничить свой вариант использования переносом в класс, который вы в настоящее время определяете, проблема все равно будет заключаться в том, что вы не сможете узнать класс, в котором определен статический метод, если все, что у вас есть, это статический метод. И это тупое ограничение Python :-/ - person Alfe; 16.11.2012