a += b 不等同于 a = a + b。

48

可能是重复问题:
为什么在列表上使用+=会表现出意外行为?

今天我发现了 Python 语言的一个有趣的“特性”,这让我苦恼不已。

>>> a = [1, 2, 3]
>>> b = "lol"
>>> a = a + b 
TypeError: can only concatenate list (not "str") to list
>>> a += b
>>> a
[1, 2, 3, 'l', 'o', 'l']

怎么会这样?我认为这两个应该是等价的!更糟的是,这就是我曾经费了很大功夫才排查出来的代码。

>>> a = [1, 2, 3]
>>> b = {'omg': 'noob', 'wtf' : 'bbq'}
>>> a = a + b
TypeError: can only concatenate list (not "dict") to list
>>> a += b
>>> a
[1, 2, 3, 'omg', 'wtf']

我靠!我的代码里有列表和字典,但我竟然在没有调用.keys()的情况下把字典的键附加到了列表中。原来是这样。

我以为这两个语句是等价的。即使忽略这个,我也可以理解将字符串添加到列表中的方式(因为字符串只是字符数组),但是字典呢?也许如果它附加了一个(键,值)元组列表,那么仅获取键并将其添加到列表中似乎是完全随意的。

有人知道其中的逻辑吗?


3
这个问题因为重复被关闭了,但我不确定它是否真的是重复的——虽然答案可能相同,但问题似乎是不同的。 - Mark Ransom
这个问题也被提出过,甚至在最近几天内以其他形式被提出过。 - agf
5
如果在所有Python教程中都有提到这一点,那我就没有看到过。我已经使用Python工作了一年,写了一个编译器和一个网站,并花了很多时间阅读关于Python功能和缺陷的各种内容。老实说,我从未读到过这个,并且也没有预料到这种行为。我不认为"可变序列"是寻找"+="与"+"不同运算符的明显位置,在再次查看页面时我根本没有看到它。搜索"+="没有给我任何有用的结果。我认为这种行为并不像你想象的那么明显! - Li Haoyi
1
哇,这种行为让我浪费了多个小时来调试。虽然知道是这样的,但现在我还剩下一个问题:为什么?为什么Python会这样做? - lakerz
2个回答

42
这一直是可变性问题以及运算符重载的一个问题。C ++也没有更好的解决方法。
表达式 a + b 从绑定到 a 和 b 的对象计算出一个新列表,这些对象不会被修改。当您将其分配回 a 时,您将一个变量的绑定更改为指向新值。+应该是对称的,因此您不能添加字典和列表。
语句a += b 修改了绑定到a的现有列表。由于它不会改变对象标识,因此所有绑定到由a表示的对象的更改都是可见的。操作符+=显然不是对称的,它等同于list.extend,遍历第二个操作数。对于字典,这意味着列出键。
讨论:
如果对象不实现+=,则Python将使用+和=将其转换为等效语句。因此,两者有时是等效的,具体取决于所涉及对象的类型。
+=对引用进行修改(而不是操作数值),好处在于实现可以更有效率,而不需要相应地增加实现复杂性。
在其他语言中,您可能会使用更明显的符号。例如,在没有运算符重载的假想版本的Python中,您可能会看到:
a = concat(a, b)

对比

a.extend(a, b)

这种运算符表示法实际上只是这些内容的缩写。

额外奖励:

也可以用于其他可迭代对象。

>>> a = [1,2,3]
>>> b = "abc"
>>> a + b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "str") to list
>>> a += b
>>> a
[1, 2, 3, 'a', 'b', 'c']

能够这样做非常有用,因为你可以使用 += 将生成器附加到列表中并获取生成器的内容。不幸的是,它破坏了与 + 的兼容性,但没关系。


2
我不知道你所说的“其他可迭代对象”是什么意思,这不是他问题中的第一个例子吗? - agf
8
这里有一个相关的陷阱t = ([],); t[0] += [2, 3]。第二个语句会引发异常,但之后,t仍然是([2, 3],) - Lauritz V. Thaulow
C++ 绝对更好。在 C++ 中,如果 x 是支持 + 的可变类型(例如 std::string),那么 x += expr 总是与 x = x + expr 具有相同的含义,只是可能更有效率。当然,您可以在自己的类中重载 += 来执行任何操作,但是标准库类型都不会像 Python 那样表现得疯狂。 - benrg
@benrg:在C++中,说x += y总是和x = x + y有相同的意义是绝对不正确的。 - Dietrich Epp
我举了一个例子,它具有相同的意思;你能举一个例子,它具有不同的意思吗? - benrg
显示剩余3条评论

8
这是因为 python 列表(在你的情况下是 a)实现了 __iadd__ 方法,该方法又会调用传递参数的__iter__ 方法。
以下代码片段更好地说明了这一点:
class MyDict(dict):
    def __iter__(self):
        print "__iter__ was called"
        return super(MyDict, self).__iter__()


class MyList(list):
    def __iadd__(self, other):
        print "__iadd__ was called"
        return super(MyList, self).__iadd__(other)


a = MyList(['a', 'b', 'c'])
b = MyDict((('d1', 1), ('d2', 2), ('d3', 3)))

a += b

print a

结果是:
__iadd__ was called
__iter__ was called
['a', 'b', 'c', 'd2', 'd3', 'd1']

Python解释器会检查对象是否实现了__iadd__操作(+=),只有当没有实现时,它才会通过执行加法操作后再进行赋值来模拟它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接