关于运算符:Python中的“i + = x”与“i = i + x”不同?

When is “i += x” different from “i = i + x” in Python?

有人告诉我,+=的效果与标准的i = i +符号不同。有没有一种情况下,i += 1会与i = i + 1有所不同?


这完全取决于对象i

+=调用__iadd__方法(如果存在,如果不存在,则返回__add__方法),而+在少数情况下调用__add__方法1或__radd__方法2。

从API的角度来看,__iadd__应该被用于在适当的位置修改可变对象(返回发生变化的对象),而__add__应该返回某个对象的新实例。对于不可变对象,这两个方法都返回一个新实例,但__iadd__将把新实例放在与旧实例同名的当前命名空间中。这就是为什么

1
2
i = 1
i += 1

似乎增加了i。实际上,您会得到一个新的整数,并将其分配到"EDOCX1"(0)的顶部——丢失一个对旧整数的引用。在这种情况下,i += 1i = i + 1完全相同。但是,对于大多数可变对象,情况就不同了:

作为一个具体的例子:

1
2
3
4
5
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]

比较对象:

1
2
3
4
5
a = [1, 2, 3]
b = a
b = b + [1, 2, 3]
print a #[1, 2, 3]
print b #[1, 2, 3, 1, 2, 3]

注意,在第一个例子中,由于ba引用同一个对象,当我在b上使用+=时,它实际上更改了b(并且a也看到了这种更改——毕竟,它引用的是同一个列表)。然而,在第二种情况下,当我执行b = b + [1, 2, 3]操作时,这将获取b引用的列表,并将其与新列表[1, 2, 3]连接起来。然后,它将连接的列表存储在当前名称空间中,称为b,而不考虑之前的行是什么b

1在表达式x + y中,如果x.__add__没有实现,或者x.__add__(y)返回NotImplementedxy有不同的类型,那么x + y试图调用y.__radd__(x)。所以,如果你有

埃多克斯1〔33〕

如果Foo不实现__add____iadd__,那么这里的结果与

埃多克斯1〔37〕

2在表达foo_instance + bar_instance时,如果bar_instance的类型是foo_instance类型的一个子类(例如issubclass(Bar, Foo)),bar_instance.__radd__将在foo_instance.__add__之前被尝试。这样做的理由是,从某种意义上说,BarFoo是一个"更高层次"的对象,所以Bar应该可以选择推翻Foo的行为。


在封面下,i += 1的做法如下:

1
2
3
4
try:
    i = i.__iadd__(1)
except AttributeError:
    i = i.__add__(1)

虽然i = i + 1做了类似的事情:

1
i = i.__add__(1)

这有点过于简单化,但您可以理解为:python通过创建__iadd__方法和__add__,为类型提供了一种特别处理+=的方法。

其目的是,像list这样的可变类型将在__iadd__中发生变异(然后返回self,除非您正在做一些非常棘手的事情),而像int这样的不可变类型将不会实现它。

例如:

1
2
3
4
5
>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]

因为l2l1是同一对象,而你突变了l1,你也突变了l2

但是:

1
2
3
4
5
>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]

在这里,您没有突变l1;相反,您创建了一个新的列表,l1 + [3],并将名称l1反弹到指向它,使l2指向原始列表。

(在+=版本中,您也在重新绑定l1,只是在这种情况下,您将它重新绑定到它已经绑定到的同一个list版本,因此您通常可以忽略该部分。)


下面是一个直接比较i += xi = i + x的例子:

1
2
3
4
5
6
7
8
9
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]

如果你只是处理文字,那么i += 1i = i + 1的行为是一样的。