How to implement __iadd__ for a Python property
Asked Answered
F

4

16

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?

Furnish answered 16/8, 2012 at 13:13 Comment(0)
C
11

__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
Chamberlin answered 16/8, 2012 at 13:20 Comment(2)
I'm a bit confused by this code snippet. Shouldn't the property getter return something?Turanian
@Turanian oh yeah, oops. Got a bit confusing with the problems of subclassing int. Thanks!Chamberlin
C
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.

Castle answered 16/8, 2012 at 13:40 Comment(4)
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.Mestas
@MarkLodato Please verify your claims before shouting them out. It is exactly as weitten in the answer. Try it out.Ayeshaayin
@MarkLodato Look here: 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.Ayeshaayin
@glglgl, you're right. Sorry about that. Reference: docs.python.org/3/reference/…Mestas
D
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.

Dagley answered 16/8, 2012 at 13:19 Comment(0)
F
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.

Furnish answered 16/8, 2012 at 13:18 Comment(2)
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.Turanian
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.Turanian

© 2022 - 2024 — McMap. All rights reserved.