列表中的列表意外地在子列表中反映变化

951 投票
19 回答
79105 浏览
提问于 2025-04-11 09:32

我创建了一个列表的列表:

>>> xs = [[1] * 4] * 3
>>> print(xs)
[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]

然后,我改变了其中一个最里面的值:

>>> xs[0][0] = 5
>>> print(xs)
[[5, 1, 1, 1], [5, 1, 1, 1], [5, 1, 1, 1]]

为什么每个子列表的第一个元素都变成了 5 呢?


另请参见:

19 个回答

74

其实,这正是你所期待的结果。我们来拆解一下这里发生了什么:

你写了

lst = [[1] * 4] * 3

这相当于:

lst1 = [1]*4
lst = [lst1]*3

这意味着 lst 是一个包含3个元素的列表,这3个元素都指向 lst1。所以接下来的两行代码是等价的:

lst[0][0] = 5
lst1[0] = 5

因为 lst[0] 其实就是 lst1

如果你想得到想要的效果,可以使用列表推导式:

lst = [ [1]*4 for n in range(3) ]

在这种情况下,每次对 n 的表达式都会重新计算,从而生成一个不同的列表。

183
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for _ in range(size)]

这是一个关于如何在Python中创建一个矩阵的例子。我们先定义一个大小为3的变量,然后用这个大小来生成一个矩阵。这个矩阵的每个元素最开始都是0。

具体来说,第一行代码是设置矩阵的大小为3。第二行代码创建了一个3x3的矩阵,里面的每个位置都填充了0。第三行代码则是用一种更简洁的方式来生成这个矩阵。

如果你想更直观地理解这个过程,可以点击这里查看这个链接,它会用图形化的方式展示代码的运行过程。

下面的图片展示了框架和对象的关系,可以帮助你更好地理解这个概念:

Frames and Objects

806

当你写 [x]*3 的时候,实际上得到的是一个列表 [x, x, x]。也就是说,这个列表里有三个指向同一个 x 的引用。当你修改这个单独的 x 时,所有三个引用都会看到这个变化:

x = [1] * 4
xs = [x] * 3
print(f"id(x): {id(x)}")
# id(x): 140560897920048
print(
    f"id(xs[0]): {id(xs[0])}\n"
    f"id(xs[1]): {id(xs[1])}\n"
    f"id(xs[2]): {id(xs[2])}"
)
# id(xs[0]): 140560897920048
# id(xs[1]): 140560897920048
# id(xs[2]): 140560897920048

x[0] = 42
print(f"x: {x}")
# x: [42, 1, 1, 1]
print(f"xs: {xs}")
# xs: [[42, 1, 1, 1], [42, 1, 1, 1], [42, 1, 1, 1]]

要解决这个问题,你需要确保在每个位置都创建一个新的列表。一个方法是:

[[1]*4 for _ in range(3)]

这样每次都会重新计算 [1]*4,而不是只计算一次,然后用三个引用指向同一个列表。


你可能会想,为什么 * 不能像列表推导那样创建独立的对象。这是因为乘法运算符 * 只对对象进行操作,而不去看表达式。当你用 *[[1] * 4] 乘以 3 时,* 只看到 [[1] * 4] 计算出的那个一元素列表,而不是 [[1] * 4 的表达式文本。* 并不知道如何复制那个元素,也不知道如何重新计算 [[1] * 4],更不知道你想要复制,通常情况下,甚至可能没有办法复制那个元素。

因此,* 唯一能做的就是对现有的子列表创建新的引用,而不是尝试创建新的子列表。其他做法要么不一致,要么需要对语言的基本设计做重大改动。

相比之下,列表推导在每次迭代时都会重新计算元素表达式。[[1] * 4 for n in range(3)] 每次都会重新计算 [1] * 4,原因和 [x**2 for x in range(3)] 每次重新计算 x**2 是一样的。每次计算 [1] * 4 都会生成一个新的列表,所以列表推导正好做了你想要的事情。

顺便提一下,[1] * 4 也不会复制 [1] 的元素,但这没关系,因为整数是不可变的。你不能像 1.value = 2 这样把 1 变成 2。

撰写回答