How can I make a read-only property mutable?

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

Upvotes: 4

Views: 470

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1122032

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

Upvotes: 5

Related Questions