列表中的列表意外地在子列表中反映变化
我创建了一个列表的列表:
>>> 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
呢?
另请参见:
如何克隆一个列表,以便在赋值后不会意外改变? 这里有一些解决这个问题的方法
字典列表在每次迭代中只存储最后添加的值 这是一个类似的问题,涉及字典的列表
如何在Python中初始化一个空列表的字典? 这是一个类似的问题,涉及列表的字典
19 个回答
其实,这正是你所期待的结果。我们来拆解一下这里发生了什么:
你写了
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
的表达式都会重新计算,从而生成一个不同的列表。
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for _ in range(size)]
这是一个关于如何在Python中创建一个矩阵的例子。我们先定义一个大小为3的变量,然后用这个大小来生成一个矩阵。这个矩阵的每个元素最开始都是0。
具体来说,第一行代码是设置矩阵的大小为3。第二行代码创建了一个3x3的矩阵,里面的每个位置都填充了0。第三行代码则是用一种更简洁的方式来生成这个矩阵。
如果你想更直观地理解这个过程,可以点击这里查看这个链接,它会用图形化的方式展示代码的运行过程。
下面的图片展示了框架和对象的关系,可以帮助你更好地理解这个概念:
当你写 [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。