15

I'm trying to create a Python property where in-place adding is handled by a different method than retrieving the value, adding another value and reassigning. So, for a property x on an object o,

o.x += 5

should work differently than

o.x = o.x + 5

The value of o.x should be the same in the end, so as not to confuse people's expectations, but I want to make the in-place add more efficient. (In reality the operation takes a lot more time than simple addition.)

My first idea was to define, in the class,

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

But this raises an AttributeError, presumably because property implements __slots__?

My next attempt uses a descriptor object:

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

As you can see, the special __iadd__ method is not called. I'm having trouble understanding why this is, although I surmise that the object's __getattr__ is somehow bypassing it.

How can I do this? Am I not getting the point of descriptors? Do I need a metaclass?

ptomato
  • 56,175
  • 13
  • 112
  • 165

4 Answers4

9

__iadd__ will only be looked for on the value returned from __get__. You need to make __get__ (or the property getter) return an object (or a proxy object) with __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
ecatmur
  • 152,476
  • 27
  • 293
  • 366
7

The += operator in the line

o.x += 5

is translated to

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

The attribute lookup on the right-hand side is translated to

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

As you can see, __iadd__() is called on the return value of the attribute lookup, so you need to implement __iadd__() on the returned object.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • This is not correct. Either `o.x.__iadd__(5)` is called, or if `o.x` does not have and `__iadd__` method, `o.x = o.x + 5` is called. That is, `o.x = o.x.__iadd__(5)` is never used, because `__iadd__` does not return anything. – Mark Lodato Aug 28 '13 at 12:59
  • @MarkLodato Please verify your claims before shouting them out. It is exactly as weitten in the answer. Try it out. – glglgl Dec 10 '13 at 22:41
  • @MarkLodato Look here: http://codepad.org/wbURpQJT It shows how `__iadd__()` is called when doing `+=` (ability to assing somethin completely new to the left hand name), and it shows as well what `list.__iadd__()` returns: its `self`. – glglgl Dec 10 '13 at 22:47
  • @glglgl, you're right. Sorry about that. Reference: http://docs.python.org/3/reference/datamodel.html#emulating-numeric-types – Mark Lodato Dec 12 '13 at 03:01
3

Why not something like the following example. Basically the idea is to let the Bar class ensure the stored value for property x is always a Foo object.

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: Extended example to show how to use it as a property. I first misunderstood the problem slightly.

muksie
  • 12,565
  • 1
  • 19
  • 14
1

To inspire you, here's a less-than-ideal solution which is the best I've managed to come up with so far:

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'

The big disadvantage being, that you have to implement the full complement of numerical magic methods on IAddObj if you want the property to behave anything like a number. Having IAddObj inherit from int doesn't seem to let it inherit the operators, either.

ptomato
  • 56,175
  • 13
  • 112
  • 165
  • You say "Having IAddObj inherit from `int` doesn't seem to let it inherit the operators, either." That's not quite true. The problem is that if you don't override all the other operators, then the type of `IAddObj(5) + 5` is `int`, not `IAddObj`, because ints are immutable, and all operations always return new ints. However, there is indeed a way around this using metaclasses. [See this answer](http://stackoverflow.com/q/10771010/577088). – senderle Aug 16 '12 at 14:14
  • However, thinking about it more, I believe that ecatmur and muksie offer solutions that will be less complex and more suited to your actual goals. – senderle Aug 16 '12 at 14:49