I have two classes, one with an "in-place operator" override (say +=
) and another that exposes an instance of the first through a @property
. (Note: this is greatly simplified from my actual code to the minimum that reproduces the problem.)
class MyValue(object):
def __init__(self, value):
self.value = value
def __iadd__(self, other):
self.value += other
return self
def __repr__(self):
return str(self.value)
class MyOwner(object):
def __init__(self):
self._what = MyValue(40)
@property
def what(self):
return self._what
Now, when I try to use that operator on the exposed property:
>>> owner = MyOwner()
>>> owner.what += 2
AttributeError: can't set attribute
From what I've found this is to be expected, since it's trying to set the property on owner
. Is there some way to prevent setting the property to a new object, while still allowing me to (in-place) modify the object behind it, or is this just a quirk of the language?
(See also this question, but I'm trying to go the other way, preferably without reverting to old-style classes because eventually I want it to work with Python 3.)
In the meantime I've worked around this with a method that does the same thing.
class MyValue(object):
# ...
def add(self, other):
self.value += other
>>> owner = MyOwner()
>>> owner.what.add(2)
>>> print(owner.what)
42
This is a quirk of the language; the object += value
operation translates to:
object = object.__iadd__(value)
This is necessary because not all objects are mutable. Yours is, and correctly returns self
resulting in a virtual no-op for the assignment part of the above operation.
In your case, the object
in question is also an attribute, so the following is executed:
owner.what = owner.what.__iadd__(2)
Apart from avoiding referencing object.what
here on the left-hand side (like tmp = owner.what; tmp += 2
), there is a way to handle this cleanly.
You can easily detect that the assignment to the property concerns the same object and gate on that:
class MyOwner(object):
def __init__(self):
self._what = MyValue(40)
@property
def what(self):
return self._what
@what.setter
def what(self, newwhat):
if newwhat is not self._what:
raise AttributeError("can't set attribute")
# ignore the remainder; the object is still the same
# object *anyway*, so no actual assignment is needed
Demo:
>>> owner = MyOwner()
>>> owner.what
40
>>> owner.what = 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 24, in what
AttributeError: can't set attribute
>>> owner.what += 2
>>> owner.what
42