python列表连接中的奇怪行为

Weird behavior in python list concatenation

本问题已经有最佳答案,请猛点这里访问。

我创建一个python列表

1
>>> list1 = ['a', 'b', 'c']

并设置

1
>>> list2 = list1

现在我执行两个类似的操作,分别是list1list2

1
2
3
4
5
>>> list1 = list1 + [1, 2, 3]
>>> list1
['a', 'b', 'c', 1, 2, 3]
>>> list2
['a', 'b', 'c']

1
2
3
4
5
>>> list2 += [1,2,3]
>>> list1
['a', 'b', 'c', 1, 2, 3]
>>> list2
['a', 'b', 'c', 1, 2, 3]

但两种情况的结果都不一样。原因是什么?


python for list中的+=操作符实际上在内部调用list.extend()函数,因此列表被适当扩展。

而当我们执行+串联操作符时,会创建并返回一个新的列表,因此在list1中存在的实际列表不会更改,而是list1现在指向一个新的列表。


这是因为您在第一个操作中为list1分配了一个新对象,而您正在用第二个操作更改分配给list2的原始对象。

如果使用id()检查分配给变量的对象的ID,则更容易理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> list1 = ['a', 'b', 'c']
>>> id(list1)
4394813200
>>> list2 = list1
>>> id(list2)
4394813200 # same id
>>> list1 = list1 + [1, 2, 3]
>>> id(list1)
4394988392 # list1 now references another object
>>> list1
['a', 'b', 'c', 1, 2, 3]
>>> id(list2)
4394813200 # list2 still references the old one
>>> list2
['a', 'b', 'c']
>>> list2 += [1,2,3]
>>> id(list2)
4394813200 # list2 still references the old one
>>> list2
['a', 'b', 'c', 1, 2, 3]
>>> id(list1)
4394988392
>>> list1
['a', 'b', 'c', 1, 2, 3]


这背后的原因是+=+调用了类的两种不同方法:__iadd__方法和__add__方法。

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

1
2
i = 1
i += 1

实际上,你得到了一个新的整数,并在"i"的顶部分配它——丢失了一个对旧整数的引用。在这种情况下,i+=1与i=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]

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


根据内德·巴切尔德在2015年Pycon上的演讲:

清单上的__iadd__被实现为扩展实际实例(根据ned,它是cpython的未记录行为)。在list1 = list2之后,两个名称都指向同一个实例,因此扩展实例在第二个名称下可见。

__add__实际上创建了一个基于两个输入列表的新列表。

作为证明,请考虑以下代码片段:

1
2
3
4
5
6
7
8
9
10
import dis

def f1():
    list1 += [1,2,3]

def f2():
    list1 = list1 + [1,2,3]

dis.dis(f1)
dis.dis(f2)

让我们检查输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> dis.dis(f1)
  2           0 LOAD_FAST                0 (list1)
              3 LOAD_CONST               1 (1)
              6 LOAD_CONST               2 (2)
              9 LOAD_CONST               3 (3)
             12 BUILD_LIST               3
             15 INPLACE_ADD
             16 STORE_FAST               0 (list1)
             19 LOAD_CONST               0 (None)
             22 RETURN_VALUE
>>> dis.dis(f2)
  2           0 LOAD_FAST                0 (list1)
              3 LOAD_CONST               1 (1)
              6 LOAD_CONST               2 (2)
              9 LOAD_CONST               3 (3)
             12 BUILD_LIST               3
             15 BINARY_ADD
             16 STORE_FAST               0 (list1)
             19 LOAD_CONST               0 (None)
             22 RETURN_VALUE

如你所见,+=使用INPLACE_ADD,而l1 + l2不使用。