在Python中,"i += x"何时与"i = i + x"不同?

234 投票
3 回答
63553 浏览
提问于 2025-04-17 18:54

有人告诉我,+=这个写法和普通的i = i +写法可能会有不同的效果。请问有没有什么情况是i += 1i = i + 1会不一样的呢?

3 个回答

7

这里有一个例子,直接对比了 i += xi = 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]
68

在背后,i += 1 大概是这样工作的:

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

i = i + 1 则是这样工作的:

i = i.__add__(1)

这有点简化了,但你明白意思了:Python 让不同的数据类型可以特别处理 +=,通过创建一个叫 __iadd__ 的方法,还有一个叫 __add__ 的方法。

这样做的目的是,像 list 这样的可变类型会在 __iadd__ 方法里改变自己(然后返回 self,除非你做了一些很复杂的事情),而像 int 这样的不可变类型则不会实现这个方法。

举个例子:

>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]

因为 l2l1 是同一个对象,当你改变了 l1,那么 l2 也会被改变。

但是:

>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]

在这里,你并没有改变 l1;相反,你创建了一个新的列表 l1 + [3],并把名字 l1 重新指向这个新列表,结果 l2 还是指向原来的列表。

(在 += 的情况下,你也是在重新绑定 l1,只不过那时候你是把它重新绑定到它原本就指向的同一个 list,所以通常可以忽略这一部分。)

350

这完全取决于对象 i 的类型。

+= 会调用 __iadd__ 方法(如果这个方法存在的话,如果不存在就会调用 __add__),而 + 则会调用 __add__ 方法1,在某些情况下还会调用 __radd__ 方法2

从 API 的角度来看,__iadd__ 是用来修改可变对象的,直接在原地进行修改(返回被修改的对象),而 __add__ 则应该返回一个新的实例。对于不可变对象,这两个方法都会返回一个新的实例,但 __iadd__ 会把新实例放在当前命名空间中,使用旧实例的名字。这就是为什么

i = 1
i += 1

看起来像是在增加 i 的值。实际上,你得到的是一个新的整数,并把它“放在” i 上面——失去了对旧整数的一个引用。在这种情况下,i += 1i = i + 1 是完全一样的。但是,对于大多数可变对象来说,情况就不一样了:

举个具体的例子:

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]

与之相比:

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) 返回 NotImplemented 并且 xy 的类型不同,那么 x + y 会尝试调用 y.__radd__(x)。所以,在你有

foo_instance += bar_instance

如果 Foo 没有实现 __add____iadd__,那么结果和

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2在表达式 foo_instance + bar_instance 中,如果 bar_instance.__radd__ 会在 foo_instance.__add__ 之前被尝试,前提是 bar_instance 的类型是 foo_instance 类型的子类(例如 issubclass(Bar, Foo))。这样做的原因是 Bar 在某种意义上是比 Foo 更“高级”的对象,因此 Bar 应该有机会覆盖 Foo 的行为。

撰写回答