Как реализовать __iadd__ для свойства Python

Я пытаюсь создать свойство Python, где добавление на месте обрабатывается другим методом, чем извлечение значения, добавление другого значения и переназначение. Итак, для свойства x для объекта o,

o.x += 5

должен работать иначе, чем

o.x = o.x + 5

Значение o.x должно быть таким же в конце, чтобы не путать ожидания людей, но я хочу сделать добавление на месте более эффективным. (На самом деле операция занимает гораздо больше времени, чем простое добавление.)

Моя первая идея заключалась в том, чтобы определить в классе

x = property(etc. etc.)
x.__iadd__ = my_iadd

Но это вызывает атрибут AttributeError, предположительно потому, что property реализует __slots__?

Моя следующая попытка использует объект дескриптора:

class IAddProp(object):
    def __init__(self):
        self._val = 5

    def __get__(self, obj, type=None):
        return self._val

    def __set__(self, obj, value):
        self._val = value

    def __iadd__(self, value):
        print '__iadd__!'
        self._val += value
        return self

class TestObj(object):
    x = IAddProp()
    #x.__iadd__ = IAddProp.__iadd__  # doesn't help

>>> o = TestObj()
>>> print o.x
5
>>> o.x = 10
>>> print o.x
10
>>> o.x += 5  # '__iadd__!' not printed
>>> print o.x
15

Как вы можете видеть, специальный метод __iadd__ не вызывается. У меня возникли проблемы с пониманием, почему это так, хотя я догадываюсь, что объект __getattr__ каким-то образом обходит его.

Как я могу это сделать? Разве я не получаю дескрипторов? Мне нужен метакласс?

Ответ 1

__iadd__ будет отображаться только на значение, возвращаемое с __get__. Вам нужно сделать __get__ (или свойство getter) вернуть объект (или прокси-объект) с помощью __iadd__.

@property
def x(self):
    proxy = IProxy(self._x)
    proxy.parent = self
    return proxy

class IProxy(int, object):
    def __iadd__(self, val):
        self.parent.iadd_x(val)
        return self.parent.x

Ответ 2

Оператор += в строке

o.x += 5

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

o.x = o.x.__iadd__(5)

Поиск атрибута в правой части преобразуется в

o.x = IAddProp.__get__(TestObj2.x, o, TestObj2).__iadd__(5)

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

Ответ 3

Чтобы вдохновить вас, здесь решение, меньшее, чем идеальное, лучшее, что мне удалось придумать до сих пор:

class IAddObj(object):
    def __init__(self):
        self._val = 5

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

    def __iadd__(self, value):
        print '__iadd__!'
        self._val += value
        return self._val

class TestObj2(object):
    def __init__(self):
        self._x = IAddObj()

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x._val = value

>>> o = TestObj2()
>>> print o.x
5
>>> o.x = 10
>>> print o.x
10
>>> o.x += 5
__iadd__!
>>> print o.x
15
>>> print o.x + 5  # doesn't work unless __add__ is also implemented
TypeError: unsupported operand type(s) for +: 'IAddObj' and 'int'

Большим недостатком является то, что вам нужно реализовать полный набор численных методов магии на IAddObj, если вы хотите, чтобы свойство вело себя как число. Имея IAddObj наследование от int, похоже, не позволяет ему наследовать операторы.

Ответ 4

Почему бы не что-то вроде следующего примера. В основном идея состоит в том, чтобы позволить классу Bar гарантировать, что сохраненное значение для свойства x всегда является объектом Foo.

class Foo(object):
    def __init__(self, val=0):
        print 'init'
        self._val = val

    def __add__(self, x):
        print 'add'
        return Foo(self._val + x)

    def __iadd__(self, x):
        print 'iadd'
        self._val += x
        return self

    def __unicode__(self):
        return unicode(self._val)

class Bar(object):
    def __init__(self):
        self._x = Foo()

    def getx(self):
        print 'getx'
        return self._x

    def setx(self, val):
        if not isinstance(val, Foo):
            val = Foo(val)
        print 'setx'
        self._x = val

    x = property(getx, setx)

obj = Bar()
print unicode(obj.x)
obj.x += 5
obj.x = obj.x + 6
print unicode(obj.x)

EDIT: расширенный пример, чтобы показать, как использовать его как свойство. Я сначала неправильно понял проблему.