坑日志:Python在初始化数组时奇怪的元素重用

数组是非常好用的东西。有的时候,我们想要初始化一个固定长度的数组。Python提供了一个非常简便的写法[elem_to_repeat] * num_time,如[0] * 4,会初始化一个长度为4,元素全为0的数组[0, 0, 0, 0]。这种写法在处理一维数值型、布尔型数据的时候不会产生问题。但是,通常人们会不了解这种写法的真实意义,即_将elem_to_repeat的引用_重复num_time遍来初始化数组。

例子

假如我们要初始化一个元素全为0的2*2的矩阵,在不适用任何第三方库的情况下,最容易想到的方法是[[0]*2]*2。我们在环境中运行一下:

1
2
3
>>> a = [[0] * 2] * 2
>>> a
[[0, 0], [0, 0]]

看起来没有任何问题,我们可能就接着继续往前做了。但是,如果某个时候,我们想把第1行第1个元素改成1,然后继续,会发现结果和自己认为的大相径庭。

1
2
3
>>> a[0][0] = 1
>>> a
[[1, 0], [1, 0]]

没有看错,第1行的第1个元素,以及第2行的第1个元素都变成了1。这和我们所期望的完全不一样。

分析

为什么会这样呢?出现问题的原因就在初始化使用的语句a = [[0] * 2] * 2。更准确的来说是最外层的重复。注意到list是一个可变对象,那么我们实际在最外层的数组中存储了两个同样指向[0] * 2的引用。等效的执行过程是:

1
2
inner_elem = [0] * 2
a = [inner_elem, inner_elem]

如果此时,我们对a[0][0]赋值的话,实际上改变的是inner_elem[0]的值,此时输出a的值的话,自然是之前输出的结果,因为a[0]a[1]指向的都是相同的元素。

那么内层的初始化为什么不会出现这样的问题呢?因为数值型的数据都是不可变对象,引用的方式是指向内存池中表示该数值的内存地址。所以实际表现起来会像所谓按值传递的性质——会复制传入的值的值。感兴趣的话可以参考有关Python中可变对象以及不可变对象的区别。

解决方案

那么问题的核心在于如何强迫Python创建不同的list。我们可以使用List comprehension

1
2
3
4
5
6
>>> a = [[0] * 2 for _ in range(2)]
>>> a
[[0, 0], [0, 0]]
>>> a[0][0] = 1
>>> a
[[1, 0], [0, 0]]

[[0] * 2 for _ in range(2)]会强迫Python创建两个_不同_的初始值为2个0的list。自此问题解决。