17

Python's documentation on the methods related to the in-place operators like += and *= (or, as it calls them, the augmented arithmetic assignments) has the following to say:

These methods should attempt to do the operation in-place (modifying self) and return the result (which could be, but does not have to be, self). If a specific method is not defined, the augmented assignment falls back to the normal methods.

I have two closely related questions:

  • Why is it necessary to return anything from these methods if the documentation specifies that, if implemented, they should only be doing stuff in-place anyway? Why don't the augmented assignment operators simply not perform the redundant assignment in the case where __iadd__ is implemented?
  • Under what circumstances would it ever make sense to return something other than self from an augmented assignment method?

A little experimentation reveals that Python's immutable types don't implement __iadd__ (which is consistent with the quoted documentation):

>>> x = 5
>>> x.__iadd__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__iadd__'

and the __iadd__ methods of its mutable types, of course, operate in-place and return self:

>>> list1 = []
>>> list2 = list1
>>> list1 += [1,2,3]
>>> list1 is list2
True

As such, I can't figure out what the ability to return things other than self from __iadd__ is for. It seems like it would be the wrong thing to do in absolutely all circumstances.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • 2
    `l = [1, 2, 3]; l = l.__iadd__([3, 2, 1]) + [1]; >>> l [1, 2, 3, 3, 2, 1, 1]` – smac89 Dec 31 '13 at 00:36
  • @Smac89 Nice - that's an eccentric but just about legit justification for `__iadd__` having a return value, so I guess I can check the first of my questions off. I'm still stumped by the second question, though - under what circumstances would it ever be useful or correct to return something other than `self` from the `__iadd__` method, as the docs indicate is permissible? – Mark Amery Dec 31 '13 at 00:42
  • 1
    The assignment is necessary for cases like `x[[0, 1]] += 2`, where `x` is a NumPy array and the assignment adds 2 to a list of selected indices. I can't think of any case where you'd return anything but `self`, though. – user2357112 Dec 31 '13 at 00:43
  • 2
    @Smac89: Actually, I'm not sure that's a good justification. You could just as easily argue that `l = l.sort() + [1]` should work, and it (intentionally) doesn't. – abarnert Dec 31 '13 at 01:56
  • I would like to point out that `+=` does not have a value. For example, `foo = (bar += 1)` is a **syntax error** no matter what type `bar` is. So unless you expect your clients to call `__iadd__` directly (and who in their right mind would do that?!) it **does not matter** what you return from it. – John Henckel Dec 08 '16 at 03:31
  • 1
    @JohnHenckel that's not true at all - the return value certainly matters! The return value is what gets assigned to `qux` when you do `qux += baz`. – Mark Amery Dec 08 '16 at 11:13
  • @MarkAmery, by golly you are right! i didn't believe it until I tried it myself. I agree with your OP that this is very odd. But abarnert's explanation below makes sense to me now. – John Henckel Dec 08 '16 at 15:22

2 Answers2

7

Why is it necessary to return anything from these methods if the documentation specifies that, if implemented, they should only be doing stuff in-place anyway? Why don't the augmented assignment operators simply not perform the redundant assignment in the case where __iadd__ is implemented?

One reason is to force them to be statements instead of expressions.


A bigger reason is that the assignment isn't always superfluous. In the case where the left-hand side is just a variable, sure, after mutating the object, re-binding that object to the name it was already bound to is usually not necessary.

But what about the case where the left-hand side is a more complicated assignment target? Remember that you can assign—and augmented-assign—to subscriptions, slicings, and attribute references, like a[1] += 2 or a.b -= 2. In that case, you're actually calling __setitem__ or __setattr__ on an object, not just binding a variable.


Also, it's worth noting that the "redundant assignment" isn't exactly an expensive operation. This isn't C++, where any assignment can end up calling a custom assignment operator on the value. (It may end up calling a custom setter operator on an object that the value is an element, subslice, or attribute of, and that could well be expensive… but in that case, it's not redundant, as explained above.)


And the last reason directly ties into your second question: You almost always want to return self from __ispam__, but almost always isn't always. And if __iadd__ ever didn't return self, the assignment would clearly be necessary.


Under what circumstances would it ever make sense to return something other than self from an augmented assignment method?

You've skimmed over an important related bit here:

These methods should attempt to do the operation in-place (modifying self)

Any case where they can't do the operation in-place, but can do something else, it will likely be reasonable to return something other than self.

Imagine an object that used a copy-on-write implementation, mutating in-place if it was the only copy, but making a new copy otherwise. You can't do that by not implementing __iadd__ and letting += fall back to __add__; you can only do it by implementing an __iadd__ that may make and return a copy instead of mutating and returning self. (You might do that for performance reasons, but it's also conceivable that you'd have an object with two different interfaces; the "high-level" interface looks immutable, and copies-on-write, while the "low-level" interface exposes the actual sharing.)

So, the first reason it's needed is to handle the non-in-place case.


But are there other reasons? Sure.

One reason is just for wrapping other languages or libraries where this is an important feature.

For example, in Objective C, lots of methods return a self which is usually but not always the same object that received the method call. That "not always" is how ObjC handles things like class clusters. In Python, there are better ways to do the same thing (even changing your class at runtime is usually better), but in ObjC, it's perfectly normal and idiomatic. (It's only used for init methods in Apple's current Framework, but it's a convention of their standard library that mutator methods added by NSMutableFoo always return void, just like the convention that mutator methods like list.sort always return None in Python, not part of the language.) So, if you wanted to wrap up the ObjC runtime in Python, how would you handle that?

You could put an extra proxy layer in front of everything, so your wrapper object can change up what ObjC object it's wrapping. But that means a whole lot of complicated delegation code (especially if you want to make ObjC reflection work back up through the wrapper into Python) and memory-management code, and a performance hit.

Instead, you could just have a generic thin wrapper. If you get back a different ObjC object than you started with, you return the wrapper around that thing instead of the wrapper around the one you started with. Trivial code, memory management is automatic, no performance cost. As long as the users of your wrapper always do a += b instead of a.__iadd__(b), they will see no difference.

I realize that "writing a PyObjC-style wrapper around a different ObjC framework library than Apple's Foundation` is not exactly an every-day use case… but you already knew that this is a feature you don't use every day, so what else would you expect?

A lazy network object proxy might do something similar—start with a tiny moniker object, swap that out for a full proxy object the first time you try to do something to it. You can probably think of other such examples. You will probably never write any of them… but if you had to, you could.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • "This isn't C++, where any assignment can end up calling a custom assignment operator" - unless you're doing `x.y += z` or `x[y] += z`, in which case it could actually be pretty expensive. – user2357112 Dec 31 '13 at 01:07
  • Interesting answer, a lot that I hadn't thought of here. A few things I'm not clear on. First, what do you mean by *One reason is to force them to be statements instead of expressions*? Secondly, can you suggest a plausible scenario in which mutating `self` in-place is impossible but 'something else' is possible and reasonable? I can't think of one, but I'm not sure if this is a defect of my imagination. Thirdly, I'm wondering if you could expand on what Objective-C methods it would be reasonable to translate to `__ispam__` methods? – Mark Amery Dec 31 '13 at 01:07
  • Forgive me if I'm wrong, but Objective-C doesn't have operator overloading, and the *return a `self` which is usually but not always the same object that received the method call* thing I thought was only for `init` methods? What am I missing? – Mark Amery Dec 31 '13 at 01:08
  • @user2357112: OK, true, you, but if your `__setitem__(idx, value)` is deep-copying `value`, the weirdness is with your class, not with `value`'s. – abarnert Dec 31 '13 at 01:10
  • 1
    @abarnert: `x[index_list] += 2`, where `x` is a NumPy array. – user2357112 Dec 31 '13 at 01:12
  • @MarkAmery: (1) I mean that if `a += 2` didn't assign to `a`, there would be no reason for it to be a statement instead of an expression. Which would be bad, because there is a _deeper_ reason for it to be a statement instead of an expression, but it would be harder to explain and justify. – abarnert Dec 31 '13 at 01:12
  • @user2357112: The whole point of that case is that it _doesn't_ iterate the array to create a temporary list, add 2 to each element, and setitem the result back. If it did that, the setitem part would would be the least of your worries. – abarnert Dec 31 '13 at 01:14
  • 1
    @abarnert: Actually, that's pretty much what it does. It's a temporary array, not a list, but it really does make a big temporary array of values, add 2 to them, and assign them back. This sometimes bites people when they expect `x[[0, 0]] += 2` to add 4 to `x[0]`. – user2357112 Dec 31 '13 at 01:16
  • 1
    @MarkAmery: (2) A copy-on-write object, where you make millions of copies (possibly in a tree rather than a flat line), would be one example of where you might want to make a copy and return something else if there were other references, but mutate in-place (saving a lot of copying time) if there weren't. – abarnert Dec 31 '13 at 01:17
  • 1
    @MarkAmery: (3) ObjC _does_ have limited operator overloading nowadays, but that's not relevant; any decent Python wrapper is probably going to want to expose ObjC features as operators, so you can write `a[3]` instead of `a.objectAtIndex_(3)` whenever possible. – abarnert Dec 31 '13 at 01:18
  • 1
    @MarkAmery: (4) There's nothing in the ObjC language that makes that only for `-init` methods; it's a convention in Foundation—just like the convention in Python's stdlib—that methods added by `NSMutableFoo` return `void`. Look at earlier mutable-string classes that returned `self` from `appendString:` so they could change classes if needed; at some point NeXT decided the benefit of doing that was not as nice as the benefit of following the convention everywhere. – abarnert Dec 31 '13 at 01:23
  • @abarnert I'm not at all sold on your point (1), but great ideas and knowledge with points (2), (3) and (4). Many thanks for your time and insight. – Mark Amery Dec 31 '13 at 01:27
  • @user2357112: You're still missing the point: it's not an operation on the _value_ that's slow, it's an operation on _your type_ (or, rather, NumPy's `ndarray` type). That's completely unlike languages like C++, where assigning a new value to a member of a vector can be slow because _the value is slow to copy_, not because of anything about the vector. Does that make sense? – abarnert Dec 31 '13 at 01:30
  • 1
    @user2357112: Meanwhile, I realize somewhere I was being misleading, thanks to you. A better way to put it is that a _superfluous_ assignment isn't going to be expensive, while an expensive assignment isn't going to be _superfluous_ unless you've done something wrong. – abarnert Dec 31 '13 at 01:30
  • @user2357112: OK, I've edited the answer to no longer just assume the subscription/slicing/attribute issue is taken as read, and to make the distinction I thought was obvious but wasn't. Does it look better to you now? – abarnert Dec 31 '13 at 01:38
-2

To answer your second question, you return something other than self when you are dealing with immutable objects.

Take this example:

>>> foo = tuple([1, 2, 3])
>>> foo
(1, 2, 3)

Now I try to mutate the object in place:

>>> foo += (4,)
>>> foo
(1, 2, 3, 4)

Can't believe that worked!...now we try to call its __iadd__ method:

>>> foo.__iadd__((5,))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute '__iadd__'

It doesn't have one! Strange...now you must wonder to yourself, what happened when we did foo += (4,)?

Well essentially what happened is that when such in-place modification is attempted on immutable objects, python uses the objects __add__ method to carry out the operation, i.e. a new object is created. So what actually happened was that when we did this:

>>> foo += (4,)

This happened:

>>> foo = foo + (4,)

See this

So when you attempt to use += or any other inplace modification on an immutable object, python will use the non-mutating equivalent of that operator and you will get back a new object. Another example:

>>> foo += (5,) + (6,)
>>> foo
(1, 2, 3, 4, 5, 6)

The above translates to:

foo = foo + (5,) + (6,)

Hope that answers your question

Community
  • 1
  • 1
smac89
  • 39,374
  • 15
  • 132
  • 179
  • 6
    No, when you deal with immutable objects you don't implement `__iadd__` at all. Just implement `__add__` which will be used to override += instead – wim Dec 31 '13 at 01:07
  • 1
    Thanks for the attempt - and your earlier comment on the question was genuinely helpful - but -1 to this answer for the reason given by wim. There's a clear error in justifying having `__iadd__` return something other than `self` by appealing to the case of types that don't even implement `__iadd__`. – Mark Amery Dec 31 '13 at 01:11
  • @MarkAmery `...If a specific method is not defined, the augmented assignment falls back to the normal methods.For instance, to execute the statement x += y, where x is an instance of a class that has an __iadd__() method, x.__iadd__(y) is called. If x is an instance of a class that does not define a __iadd__() method, x.__add__(y) and y.__radd__(x) are considered, as with the evaluation of x + y.` – smac89 Dec 31 '13 at 01:15
  • 1
    After your edit, this answer is no longer wrong, but is no longer relevant either - it's mostly about the `+=` operator now rather than the `__iadd__` method. I'm asking about the method, not the operator. You haven't provided an example of a case where it makes sense to *actually implement* `__iadd__` but have it return something other than `self`. – Mark Amery Dec 31 '13 at 01:15
  • @MarkAmery, can you give an example where `__iadd__` is not the same as `+=`? – smac89 Dec 31 '13 at 01:18
  • 1
    @Smac89 Well yeah - the case in which `__iadd__` is not implemented and `+=` falls back on `__add__`, as described in the docs you quoted. My question is when you would want to write a custom `__iadd__` method that returned something other than `self`, not when `+=` might assign something other than `self`. – Mark Amery Dec 31 '13 at 01:22
  • @MarkAmery: This answer could be relevant. If you're inheriting from a class that implements __iadd__, but want your subclass to be immutable. – Falmarri Dec 31 '13 at 01:47
  • 1
    @Falmarri: How would this answer help there? Sure, it's useful to know that you can block `__iadd__` by just overriding it with a method that returns `NotImplemented`, or (in 2.x only, I think) by interposing an extra class to sabotage base class attribute lookup, but this answer wouldn't help you find that out, would it? – abarnert Dec 31 '13 at 01:53
  • @abarnet: You wouldn't raise NotImplemented. You would presumably call `__add__` manually. But I'm not saying it's a great idea, just answering the question why you would EVER want to do it. – Falmarri Jan 08 '14 at 19:10