pythonpython-2.7propertiesnew-style-classaugmented-assignment

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

Solution

  • 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