2

So I came across this nasty bug today in my code. I've just recently been using Python properties, and used it to preserve the old value of the property in another variable whenever the property was updated. So when the setter function is called, I needed the old value to save, and then the new value to transfer.

It turns out that when using += on a property, the __iadd__ method is called, updating the property in-place, then the setter is called. But by that time the old value is lost. Here's some code illustrating the problem.

class Container(object):
    def __init__(self, val):
        self.val = val

    def __add__(self, other):
        print('in __add__', other)
        return Container(self.val + other.val)

    def __iadd__(self, other):
        print('in __iadd__:', other)
        self.val += other.val
        return self

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


class Test:
    def __init__(self):
        self._val = Container(0)

    @property
    def val(self):
        return self._val

    @val.setter
    def val(self, values):
        print("changing from {} to {}".format(self._val, values))
        self._val = values

test = Test()
print('val=', test.val)
test.val = Container(2)
print('val=', test.val)
test.val = test.val + Container(1)
print('val=', test.val)
test.val += Container(1)
print('val=', test.val)
test.val.__iadd__(Container(1))
print('val=', test.val)

When run it gives this result:

val= 0
changing from 0 to 2
val= 2
in __add__ 1
changing from 2 to 3
val= 3
in __iadd__: 1
changing from 4 to 4
val= 4
in __iadd__: 1
val= 5

It says it's changing 4 to 4 but it is really changing 3 to 4 and that was missed. So I'm wondering if this is a bug, I'm missing something, or I am just not supposed to use += if I'm dealing with a property and need the old value.

TheLoneMilkMan
  • 233
  • 1
  • 7
  • You might want to check out http://stackoverflow.com/questions/11987949/how-to-implement-iadd-for-a-python-property – Jan Feb 01 '17 at 07:09
  • That almost helped. If I just replaced `__iadd__` with `__add__` on the objects then it would fix it. But just my luck `__Iadd__` is a ready-only attribute for numpy arrays. So I just got rid of the setter and made it a non-property set function. – TheLoneMilkMan Feb 01 '17 at 16:44

1 Answers1

0

Tricky. You could cache the old value in the getter. That's a bit annoying because the caching operation is performed on every call of the getter, rather than on the setter calls. But anyway...

class Container(object):
    def __init__(self, val):
        self.val = val

    def __add__(self, other):
        print('in __add__', self.val, other)
        return Container(self.val + other.val)

    def __iadd__(self, other):
        print('in __iadd__:', self.val, other)
        self.val += other.val
        return self

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

class Test:
    def __init__(self):
        self._val = Container(0)
        self._prev_val = self._val.val

    @property
    def val(self):
        self._prev_val = self._val.val
        return self._val

    @val.setter
    def val(self, values):
        print("changing from {} to {}".format(self._prev_val, values))
        self._val = values

test = Test()
print('val=', test.val)
test.val = Container(2)
print('val=', test.val)
test.val = test.val + Container(1)
print('val=', test.val)
test.val += Container(1)
print('val=', test.val)
test.val.__iadd__(Container(1))
print('val=', test.val)

output

val= 0
changing from 0 to 2
val= 2
in __add__ 2 1
changing from 2 to 3
val= 3
in __iadd__: 3 1
changing from 3 to 4
val= 4
in __iadd__: 4 1
val= 5
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182