坑日志: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 | 0] * 2] * 2 a = [[ |
看起来没有任何问题,我们可能就接着继续往前做了。但是,如果某个时候,我们想把第1行第1个元素改成1,然后继续,会发现结果和自己认为的大相径庭。
1 | 0][0] = 1 a[ |
没有看错,第1行的第1个元素,以及第2行的第1个元素都变成了1。这和我们所期望的完全不一样。
¶分析
为什么会这样呢?出现问题的原因就在初始化使用的语句a = [[0] * 2] * 2
。更准确的来说是最外层的重复。注意到list
是一个可变对象,那么我们实际在最外层的数组中存储了两个同样指向[0] * 2
的引用。等效的执行过程是:
1 | inner_elem = [0] * 2 |
如果此时,我们对a[0][0]
赋值的话,实际上改变的是inner_elem[0]
的值,此时输出a
的值的话,自然是之前输出的结果,因为a[0]
和a[1]
指向的都是相同的元素。
那么内层的初始化为什么不会出现这样的问题呢?因为数值型的数据都是不可变对象,引用的方式是指向内存池中表示该数值的内存地址。所以实际表现起来会像所谓按值传递的性质——会复制传入的值的值。感兴趣的话可以参考有关Python中可变对象以及不可变对象的区别。
¶解决方案
那么问题的核心在于如何强迫Python创建不同的list。我们可以使用List comprehension
1 | 0] * 2 for _ in range(2)] a = [[ |
[[0] * 2 for _ in range(2)]
会强迫Python创建两个_不同_的初始值为2个0的list。自此问题解决。