1

This question deals with __iadd__ on Python read-write properties. However, I'm struggling to find the solution for read-only properties.

In my MWE we have a read-only property Beta.value, returning an Alpha instance. I imagine I should be able to use __iadd__ on Beta.value because the returned value is mutated in-place, and no change is made to Beta itself, much like the "beta.value.content +=" line preceding it. However the following code crashes with an AttributeError: can't set attribute.

Is it possible to use __iadd__ on read-only properties?

class Alpha:
    def __init__( self, content : int ) -> None:
        self.content : int = content


    def __iadd__( self, other : int ) -> "Alpha":
        self.content += other
        return self


class Beta:
    def __init__( self ):
        self.__value: Alpha = Alpha(1)


    @property
    def value( self ) -> Alpha:
        return self.__value


beta = Beta()
beta.value.content += 2
beta.value += 2
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
c z
  • 7,726
  • 3
  • 46
  • 59

3 Answers3

3

It can be tricked by adding a special setter for the property that only accepts the original object.

Class Beta would become:

class Beta:
    def __init__( self ):
        self.__value: Alpha = Alpha(1)

    def _get_val( self ) -> Alpha:
        return self.__value
    def _set_val( self, val: Alpha):
        if not (val is self.__value):            # only accept the existing object
            raise AttributeError("can't set attribute")
    value = property(_get_val, _set_val)

With that hack/trick on, you can successfully use:

>>> beta = Beta()
>>> beta.value.content
1
>>> beta.value = Alpha(2)               # property IS read only
Traceback (most recent call last):
  File "<pyshell#86>", line 1, in <module>
    beta.value = Alpha(2)
  File "<pyshell#78>", line 9, in _set_val
    raise AttributeError("can't set attribute")
AttributeError: can't set attribute
>>> beta.value.content                  # and was not changed by an assignment attempt
1
>>> beta.value += 2                     # but accepts augmented assignment
>>> beta.value.content
3
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
0

Something that people often forget with in-place operators is that their use always involves an assignment. You can see this intuitively in Alpha.content (or any int or str really): integers are immutable, but the operation works. It's much easier to forget about this step for something like Alpha or list, where the in-place operator just returns self. Just remember that the operator can return anything at all, and the result has to be bound to the original name. What's happening here is basically this:

x = beta.value
x = operator.iadd(x, 2)      # Totally fine
beta.value = x # You can  imagine how this would be a problem...

A direct consequence of this is that you'll see the changes in beta.value despite the error.

You're always welcome to bypass the reassignment by first assigning to a temporary variable, i.e., running the first two lines shown above explicitly. Just remember that while in your case Alpha is mutable and really modifies itself in-place, that's not a requirement for the general case:

x = beta.value
x += 2

works as intended. However,

x = beta.value.content
x += 2

does not, since int.__iadd__ inevitably returns a new reference.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
0

Operator += works with the whole assignement, the whole line. Therefore there is a ambiguity: is your object calling __iadd__ or a setter property ? Check this out:

This does not work:

beta.value += 2  # calls setter property (which does not exist)

But this works:

value = beta.value  # calls getter property
value += 2  # calls __iadd__ (upon the returned object from the getter)

NB: This frustrates me but it has to be like that; otherwise, how would we know which method is called and how will the code behave ?

Have fun :)

Valentin Fabianski
  • 646
  • 1
  • 5
  • 12