-1

Background

If a user-defined object has an immutable object as an attribute, then using the += operator on the attribute has a different effect to using it on another variable pointing to that attribute:

class A():
    def __init__(self):
        # Set self.x to be an (immutable) int
        self.x = 5

a = A()

Now a.x == 5

a.x += 1

Now a.x points to a different object, and a.x == 6.

x = a.x

Now x and a.x point to the same object: id(x) == id(a.x) and x == a.x == 6

x += 1

Now x and a.x point to different objects, id(x) != id(a.x) and x == 7 and a.x == 6

My question

Is it possible to implement a class B that has an attribute a such that the += operator works in the same way on b.a.x? i.e. I want the following behaviour:

Requirement 1

b = B()

Now I would like b.a.x == 5.

Requirement 2

b.a.x += 1

Now I would like b.a.x == 6.

Requirement 3

a = b.a
a.x += 1

Now I would like b.a.x == 6. I don't want incrementing a.x to affect b.a.x.

Requirement 4

a = A()
b.a = a

Now I would like b.a.x == 5.

Requirement 5

x = b.a.x
a = b.a

Now I would like x == 5 and a.x == 5 and b.a.x == 5.

Requirement 6

x += 1

Now I would like x == 6 and a.x == 5 and b.a.x == 5. I don't want incrementing x to affect a.x or b.a.x.

To put it another way, I want to be able to use the += operator to affect b.a.x, but only when it is applied directly to b.a.x, not when it is applied to another name that is bound to the same object at the time of the operation.

What I've tried

A simple definition of class B does not work:

class B():
    def __init__:
        self.a = A()

This fails requirement 3.

However, if I change b.a to be a property that returns a new copy of a, then this doesn't work either:

class B()
    def __init__:
        self._a = A()

    @property
    def a(self):
        return copy(_a)

    @a.setter
    def a(self, value)
        self._a = value

This fails requirement 2.

I also tried implementing X as a new class with an __iadd__ method that returned a new class instance. This meant that x still acted as if it were immutable when applying +=, but I couldn't figure out how to achieve both requirements 2 and 3, above.

I'm working in Python 3.

Rich
  • 7,348
  • 4
  • 34
  • 54
  • Are you sure about the observed behavior mentioned under **Requirement 3**? Did you actually observe that behavior, or are you just saying that that is how you want it to behave? I ask this because, the language you've used there suggests that you've actually observed that behavior. That behavior goes directly against my own expectation, and my own observation. – fountainhead Feb 28 '19 at 12:02
  • @fountainhead Thanks for your continuing interest in my question! The requirements describe the behaviour I *want*. I don't know how to achieve it so that all the requirements work in the way I described. I've edited the question to try to make this clearer. – Rich Feb 28 '19 at 12:27

2 Answers2

1

That desired behavior is entirely dependent upon how the += operator is implemented for the type of x.

For example, if x points to a list object, you don't get the desired behavior, and if x points to an int object, you get that behavior.

So, there's really nothing you can or need to do with your class A or class B to get this behavior.

Whatever you do to get this behavior, has to be done in the type of x

fountainhead
  • 3,584
  • 1
  • 8
  • 17
  • Thanks for your answer. Can you give me any hints as to how to implement an `x.__iadd__` that achieves the above? – Rich Feb 28 '19 at 10:35
  • ...because using a `int` does *not* give me the desired behaviour – Rich Feb 28 '19 at 10:49
  • @Rich. I'm not sure I can help there, but the discussion in the comments section of https://stackoverflow.com/questions/1047021/overriding-in-python-iadd-method might be relevant or even useful, I think – fountainhead Feb 28 '19 at 10:59
  • Thanks: I've already seen that question though, and it doesn't help solve this specific issue. I'll add a link to my question to make it clearer I'm already aware of that one. – Rich Feb 28 '19 at 11:01
  • @Rich: As a consequence of your **Requirement 3**, your interception of `X.__iadd__()` should ensure that `b.a.x` and `a.x` end up pointing to two different `X` objects. But this can be achieved only by making `b.a` and `a` to point to two different `A` objects, even though initially (before the invocation of `X.__iadd__()`) they were pointing to the same `A` object. This seems like an impossible thing to do - but as I'm only 2 months into Python, I'm not one to rule it it out yet. I'll try to have another look at the `Resource` class example in the comments of the link I pointed to earlier. – fountainhead Mar 01 '19 at 10:57
  • Changing the binding of `a` within `__iadd__` certainly seems difficult (impossible?) but changing the binding of `b.a` might be the solution... – Rich Mar 01 '19 at 11:31
  • @Rich. But from within `a.x.__iadd__()` we won't have any reference to `b`, so how could we change the binding of `b.a`? – fountainhead Mar 01 '19 at 11:41
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/189252/discussion-between-rich-and-fountainhead). – Rich Mar 01 '19 at 11:44
0

TL;DR What you are asking is not possible and breaks many promises of the language itself.

Consider requirement 3
You created a new instance of B B() (which created instance of A in its constructor) and a reference b points to this instance.

b.a is another reference which points to instance of A already created.
This code x = b.a just creates a reference pointing to the same instance of A. Any changes made to the object will be visible through both the references pointing to it.
This makes sense since this instance of A doesn't know (and doesn't care) if if it is a member of another instance. In both the case the its behaviour should be the same. (This is a promise by python language, the object behaves the same where ever it is referenced from).

shanmuga
  • 4,329
  • 2
  • 21
  • 35
  • I think I already covered the immutability of `int` in my description. If there are any mistakes in what I wrote, could you explain further? I don't think your suggestion solves the problem. If a client writes `a = b.a` then now `a` and `b.a` are bound to the same object and subsequently `a.x += 1` will affect them both. – Rich Feb 28 '19 at 10:24
  • The lines `b = B()` and `a = b.a` are in client code: requiring customers to write `a = b.copyOfA()` instead of `a = b.a` would unfortunately defeat the purpose of this API design. Any solution must reside in the definitions of the classes B, A, (and possibly X), which *are* defined in my API code. – Rich Mar 01 '19 at 12:15