在Python中,"i += x"何时与"i = i + x"不同?
有人告诉我,+=
这个写法和普通的i = i +
写法可能会有不同的效果。请问有没有什么情况是i += 1
和i = i + 1
会不一样的呢?
3 个回答
这里有一个例子,直接对比了 i += x
和 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]
在背后,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]
因为 l2
和 l1
是同一个对象,当你改变了 l1
,那么 l2
也会被改变。
但是:
>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]
在这里,你并没有改变 l1
;相反,你创建了一个新的列表 l1 + [3]
,并把名字 l1
重新指向这个新列表,结果 l2
还是指向原来的列表。
(在 +=
的情况下,你也是在重新绑定 l1
,只不过那时候你是把它重新绑定到它原本就指向的同一个 list
,所以通常可以忽略这一部分。)
这完全取决于对象 i
的类型。
+=
会调用 __iadd__
方法(如果这个方法存在的话,如果不存在就会调用 __add__
),而 +
则会调用 __add__
方法1,在某些情况下还会调用 __radd__
方法2。
从 API 的角度来看,__iadd__
是用来修改可变对象的,直接在原地进行修改(返回被修改的对象),而 __add__
则应该返回一个新的实例。对于不可变对象,这两个方法都会返回一个新的实例,但 __iadd__
会把新实例放在当前命名空间中,使用旧实例的名字。这就是为什么
i = 1
i += 1
看起来像是在增加 i
的值。实际上,你得到的是一个新的整数,并把它“放在” i
上面——失去了对旧整数的一个引用。在这种情况下,i += 1
和 i = 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]
注意在第一个例子中,由于 b
和 a
引用的是同一个对象,当我对 b
使用 +=
时,它实际上改变了 b
(而 a
也能看到这个变化——毕竟,它引用的是同一个列表)。然而在第二种情况下,当我执行 b = b + [1, 2, 3]
时,这会把 b
所引用的列表和一个新的列表 [1, 2, 3]
连接起来。然后,它把连接后的列表存储在当前命名空间中作为 b
——完全不考虑之前 b
的值。
1在表达式 x + y
中,如果 x.__add__
没有实现,或者 x.__add__(y)
返回 NotImplemented
并且 x
和 y
的类型不同,那么 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
的行为。