Обертка класса, методы которого возвращают экземпляры этого класса

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

На основе создайте класс-оболочку для вызова функции pre и post вокруг существующих функций?, у меня есть следующее.

import copy

class Wrapper(object):
    __wraps__  = None

    def __init__(self, obj):
        if self.__wraps__ is None:
            raise TypeError("base class Wrapper may not be instantiated")
        elif isinstance(obj, self.__wraps__):
            self._obj = obj
        else:
            raise ValueError("wrapped object must be of %s" % self.__wraps__)

    def __getattr__(self, name):
        orig_attr = self._obj.__getattribute__(name)
        if callable(orig_attr):
            def hooked(*args, **kwargs):
                result = orig_attr(*args, **kwargs)
                if result == self._obj:
                    return result
                return self.__class__(result)
            return hooked
        else:
            return orig_attr

class ClassToWrap(object):
    def __init__(self, data):
        self.data = data

    def theirfun(self):
        new_obj = copy.deepcopy(self)
        new_obj.data += 1
        return new_obj

class Wrapped(Wrapper):
    __wraps__ = ClassToWrap

    def myfun(self):
        new_obj = copy.deepcopy(self)
        new_obj.data += 1
        return new_obj

obj = ClassToWrap(0)
wr0 = Wrapped(obj)
print wr0.data
>> 0
wr1 = wr0.theirfun()
print wr1.data
>> 1
wr2 = wr1.myfun()
print wr2.data
>> 2
wr3 = wr2.theirfun()
print wr3.data
>> 2

Почему же theirfun() срабатывает в первый раз, а не во второй? И wr0, и wr2 имеют тип Wrapped, и вызов wr2.theirfun() не вызывает ошибки, но не добавляет 1 к wr2.data, как ожидалось.

Извините, но я не ищу следующие альтернативные подходы:

  1. Патч обезьяны. Моя кодовая база нетривиальна, и я не знаю, как обеспечить распространение патча через сеть операторов импорта.
  2. Написание отдельных методов-оболочек для всех этих хитрых методов для каждого стороннего пакета. Их слишком много.

ETA: есть пара полезных ответов, которые ссылаются на базовый атрибут _obj вне класса Wrapper. Однако смысл этого подхода в расширяемости, поэтому эта функциональность должна быть внутри класса Wrapper. myfun должен вести себя так, как ожидается, без ссылки на _obj в своем определении.


person Dave Kielpinski    schedule 11.08.2017    source источник


Ответы (2)


Проблема в вашей реализации myfun в классе Wrapped. Вы обновляете только data член экземпляра класса, но обернутый класс (ClassToWrap экземпляр, т.е. _obj) data член устарел, используя значение из предыдущего вызова theirfun.

Вам необходимо синхронизировать значения данных в обоих экземплярах:

class Wrapper(object):
    ...
    def __setattr__(self, attr, val):
        object.__setattr__(self, attr, val)
        if getattr(self._obj, attr, self._obj) is not self._obj: # update _obj's member if it exists
            setattr(self._obj, attr, getattr(self, attr))


class Wrapped(Wrapper):
    ...
    def myfun(self):
        new_obj = copy.deepcopy(self)
        new_obj.data += 1
        return new_obj

obj = ClassToWrap(0)
wr0 = Wrapped(obj)
print wr0.data
# 0
wr1 = wr0.theirfun()
print wr1.data
# 1
wr2 = wr1.myfun()
print wr2.data
# 2
wr3 = wr2.theirfun()
print wr3.data
# 3
wr4 = wr3.myfun()
print wr4.data
# 4
wr5 = wr4.theirfun()
print wr5.data
# 5
person Moses Koledoye    schedule 11.08.2017
comment
Спасибо за этот полезный ответ! Это меня частично заводит, но посмотрите на редактирование исходного вопроса - я, очевидно, недостаточно ясно изложил это. - person Dave Kielpinski; 12.08.2017
comment
Мой код просто показывает, что пошло не так, и простое исправление. Не обязательно будет применяться в производстве. Вы можете переместить обновление члена data _obj в статический метод в классе Wrapper и украсить myfun статическим методом, чтобы обеспечить синхронизацию data между оболочкой и обернутыми экземплярами. Там также могут быть другие лучшие способы для этого. - person Moses Koledoye; 12.08.2017
comment
к сожалению, в этом случае я тоже не могу полагаться на декоратор... myfun должен работать сам по себе. - person Dave Kielpinski; 12.08.2017
comment
@DaveKielpinski Тогда вы можете синхронизировать атрибуты (например, data), общие для оболочки и обернутые (например, _obj), реализовав логику в __setattr__ в своем Wrapper. - person Moses Koledoye; 12.08.2017
comment
@DaveKielpinski См. Пример такой реализации в __setattr__. - person Moses Koledoye; 12.08.2017
comment
Большое спасибо! Это именно то, чего мне не хватало! - person Dave Kielpinski; 12.08.2017
comment
@DaveKielpinski Быстро, я обновил код с помощью getattr вместо hasattr, так как последний не работает для свойств в Python2. См. hynek.me/articles/hasattr. - person Moses Koledoye; 12.08.2017

Проблема связана с назначением new_obj.data += 1 в myfun. Проблема в том, что new_obj является экземпляром Wrapped, а не экземпляром ClassToWrap. Ваш базовый класс Wrapper поддерживает только поиск атрибутов проксируемого объекта. Он не поддерживает присвоения атрибутам. Расширенное присваивание делает и то, и другое, поэтому работает не совсем корректно.

Вы можете заставить свой myfun работать, немного изменив его:

def myfun(self):
    new_obj = copy.deepcopy(self._obj)
    new_obj.data += 1
    return self.__class__(new_obj) # alternative spelling: return type(self)(new_obj)

Другим подходом к решению проблемы было бы добавление метода __setattr__ к Wrapper, но заставить его работать правильно (без вмешательства в собственные атрибуты прокси-класса) было бы немного неудобно.

Независимо от вашей текущей проблемы, вы также потенциально допускаете утечку обернутого объекта в функции-оболочке hooked. Если метод, который вы вызвали, возвращает объект, для которого он был вызван (например, метод сделал return self), вы в настоящее время возвращаете этот объект в развернутом виде. Вероятно, вы захотите изменить return result на return self, чтобы вернуть текущую оболочку. Вы также можете захотеть проверить возвращаемое значение, чтобы увидеть, является ли это типом, который вы можете обернуть или нет. В настоящее время ваш код завершится ошибкой, если метод вернет строку, число или что-то еще, кроме экземпляра обернутого типа.

        def hooked(*args, **kwargs):
            result = orig_attr(*args, **kwargs)
            if result == self._obj:
                return self                            # fix for leaking objects
            elif isisntance(result, self.__wraps__): # new type check
                return self.__class__(result)
            else:
                return result              # fallback for non-wrappable return values
person Blckknght    schedule 11.08.2017
comment
@BlckknghtСпасибо за этот полезный ответ! Это меня частично заводит, но посмотрите на редактирование исходного вопроса - я, очевидно, недостаточно ясно изложил это. - person Dave Kielpinski; 12.08.2017
comment
Я изменил if result == self.obj: на if result is self.obj. Тест на равенство значений дал плохие результаты при обертывании массива numpy. Надеюсь, это не сломает ваш код? - person Dave Kielpinski; 12.08.2017
comment
О, я просто скопировал эту часть логики из вашего кода без особых раздумий. Использование is почти всегда будет более подходящим в этом контексте, так как вы хотите обернуть новые копии объекта, возвращаемого методом. Обратите внимание, что одна из больших проблем, с которой вы столкнетесь при проксировании объекта, такого как массив, заключается в том, что методы оператора (например, __add__) не будут использовать __getattr__, они просматриваются непосредственно в классе. Вам потребуется написать собственные версии каждого оператора, который может быть реализован базовым объектом. Вы можете проверить, будет ли проще использовать наследование. - person Blckknght; 12.08.2017
comment
Если бы я мог использовать наследование, я бы обязательно это сделал. Чрезвычайно известный сторонний пакет, который я использую, очевидно, преднамеренно написан таким образом, чтобы не поддерживать наследование. - person Dave Kielpinski; 14.08.2017