Python的引用坑

The reference problem on Python

Posted by qingshan on April 15, 2022

今天在写 Python3 的时候,定义一个二维数组。发现程序没有按照预期执行。在经过反复 debug 之后。发现是 Python 的引用问题。具体如下:

Problem

以下两种定义二位数组的方法,有什么区别:

1
2
a = [[0]] * 5
b = [[0] for _ in range(5)]

第一眼看上去,好像没有区别,都是定义了二维数组并初始化:

1
2
3
4
5
>>> a
[[0], [0], [0], [0], [0]]

>>> b
[[0], [0], [0], [0], [0]]

但是,代码跑起来以后,问题出现了:

1
2
3
4
5
6
7
8
>>> a[1][0] = a[0][0] + 1
>>> a
[[1], [1], [1], [1], [1]]

>>> b[1][0] = b[0][0] + 1
>>> b
[[0], [1], [0], [0], [0]]

从上面测试可以看出来,对 ab 做相同的指定索引位赋值操作, ab 的结果天壤之别。 a 的结果是所有子元素数组值全都变化了,但 b 只有指定值变化。

Root Cause

为什么会出现这种问题呢?其实就是 Python 的 引用 设计。如下面的例子:

1
2
3
4
5
>>> a = [1]
>>> b = a
>>> a[0] = 2
>>> print b
[2]

Python 的设计者考虑到初始化 list 的时候,可能元素数量太多,导致内存紧张。因此如果第二个变量初始化为已存在 list 一致时候,Python 只会初始化一个指针指向已存在变量,这就是引用。 这样的后果就是,同一个内存区间会同时存在多个指针,并对齐操作:

1
2
3
4
5
6
>>> a = 4 * [0]
>>> a
[0, 0, 0, 0]
>>> a = 4 * [0]
>>> [id(v) for v in a]
[4300814608, 4300814608, 4300814608, 4300814608]

可以看到,元素中每个 0 的内存地址全部都一样。但是坑来了,在一维数组中, Python 可以识别出这种问题,所以在内部会规避这种将同一内存元素全部改变的问题。

1
2
3
>>> a[0] = 1
>>> [id(v) for v in a]
[4300814640, 4300814608, 4300814608, 4300814608]

可以看到在更新后的 a 列表中,被赋值的元素已经被更换成了新的内存地址,而其他的继续保留一致。说明 Python 已经在这种场景下做了优化,用户无需接入处理。

但是, 在二维数组中,Python 就没有这种优化了:

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

所以说,这是一个大坑啊。在建立二维数组时候,一不小心就会掉进去。

Solution

解决办法,就是在新建二维数组的时候,采用下面这种写法,就可以规避。切记!!!

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

Reference

https://stackoverflow.com/questions/13058458/nested-list-indices