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?
__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
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.
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.
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.