When is "i += x" different from "i = i + x" in Python?
This depends entirely on the object i
.
+=
calls the __iadd__
method (if it exists -- falling back on __add__
if it doesn't exist) whereas +
calls the __add__
method1 or the __radd__
method in a few cases2.
From an API perspective, __iadd__
is supposed to be used for modifying mutable objects in place (returning the object which was mutated) whereas __add__
should return a new instance of something. For immutable objects, both methods return a new instance, but __iadd__
will put the new instance in the current namespace with the same name that the old instance had. This is why
i = 1
i += 1
seems to increment i
. In reality, you get a new integer and assign it "on top of" i
-- losing one reference to the old integer. In this case, i += 1
is exactly the same as i = i + 1
. But, with most mutable objects, it's a different story:
As a concrete example:
a = [1, 2, 3]
b = a
b += [1, 2, 3]
print(a) # [1, 2, 3, 1, 2, 3]
print(b) # [1, 2, 3, 1, 2, 3]
compared to:
a = [1, 2, 3]
b = a
b = b + [1, 2, 3]
print(a) # [1, 2, 3]
print(b) # [1, 2, 3, 1, 2, 3]
notice how in the first example, since b
and a
reference the same object, when I use +=
on b
, it actually changes b
(and a
sees that change too -- After all, it's referencing the same list). In the second case however, when I do b = b + [1, 2, 3]
, this takes the list that b
is referencing and concatenates it with a new list [1, 2, 3]
. It then stores the concatenated list in the current namespace as b
-- With no regard for what b
was the line before.
1In the expression x + y
, if x.__add__
isn't implemented or if x.__add__(y)
returns NotImplemented
and x
and y
have different types, then x + y
tries to call y.__radd__(x)
. So, in the case where you have
foo_instance += bar_instance
if Foo
doesn't implement __add__
or __iadd__
then the result here is the same as
foo_instance = bar_instance.__radd__(bar_instance, foo_instance)
2In the expression foo_instance + bar_instance
, bar_instance.__radd__
will be tried before foo_instance.__add__
if the type of bar_instance
is a subclass of the type of foo_instance
(e.g. issubclass(Bar, Foo)
). The rationale for this is that Bar
is in some sense a "higher-level" object than Foo
so Bar
should get the option of overriding Foo
's behavior.
Here is an example that directly compares i += x
with i = i + x
:
def foo(x):
x = x + [42]
def bar(x):
x += [42]
c = [27]
foo(c); # c is not changed
bar(c); # c is changed to [27, 42]
Under the covers, i += 1
does something like this:
try:
i = i.__iadd__(1)
except AttributeError:
i = i.__add__(1)
While i = i + 1
does something like this:
i = i.__add__(1)
This is a slight oversimplification, but you get the idea: Python gives types a way to handle +=
specially, by creating an __iadd__
method as well as an __add__
.
The intention is that mutable types, like list
, will mutate themselves in __iadd__
(and then return self
, unless you're doing something very tricky), while immutable types, like int
, will just not implement it.
For example:
>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]
Because l2
is the same object as l1
, and you mutated l1
, you also mutated l2
.
But:
>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]
Here, you didn't mutate l1
; instead, you created a new list, l1 + [3]
, and rebound the name l1
to point at it, leaving l2
pointing at the original list.
(In the +=
version, you were also rebinding l1
, it's just that in that case you were rebinding it to the same list
it was already bound to, so you can usually ignore that part.)