How to implement __iadd__ for a Python property

2019-01-26 05:56发布

问题:

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?

回答1:

__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


回答2:

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.



回答3:

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.



回答4:

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.