意外地反映在子列表中的列表更改列表

我创建了一个列表列表:

xs = [[1] * 4] * 3
# xs == [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]

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

xs[0][0] = 5
# xs == [[5, 1, 1, 1], [5, 1, 1, 1], [5, 1, 1, 1]]

为什么每个子列表的每个第一个元素都改为5

69842 次浏览
[[1] * 4] * 3

甚至:

[[1, 1, 1, 1]] * 3

创建一个引用内部[1,1,1,1] 3次的列表-而不是内部列表的三个副本,因此任何时候修改列表(在任何位置),您都会看到三次更改。

这和这个例子是一样的:

>>> inner = [1,1,1,1]>>> outer = [inner]*3>>> outer[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]>>> inner[0] = 5>>> outer[[5, 1, 1, 1], [5, 1, 1, 1], [5, 1, 1, 1]]

在那里可能不那么令人惊讶。

当你写[x]*3时,你基本上得到了列表[x, x, x]。也就是说,一个列表有3个对同一个x的引用。然后当你修改这个单一的x时,它通过对它的所有三个引用可见:

x = [1] * 4xs = [x] * 3print(f"id(x): {id(x)}")# id(x): 140560897920048print(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] = 42print(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个列表进行3次引用。


你可能想知道为什么*不能像列表推导那样制作独立对象。这是因为乘法运算符*对对象进行操作,而看不到表达式。当你使用*[[1] * 4]乘以3时,*只看到[[1] * 4]计算的1元素列表,而不是[[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。

实际上,这正是你所期望的。让我们分解一下这里发生的事情:

你写

lst = [[1] * 4] * 3

这相当于:

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

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

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

因为lst[0]只是lst1

要获得所需的行为,您可以使用列表推导:

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

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

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

实时可视化使用Python导师:

框架和对象

让我们按照以下方式重写您的代码:

x = 1y = [x]z = y * 4
my_list = [z] * 3

然后有了这个,运行以下代码以使一切更加清晰。代码的作用基本上是打印获得的对象的id,这

返回[s]对象的“身份”

并将帮助我们识别它们并分析会发生什么:

print("my_list:")for i, sub_list in enumerate(my_list):print("\t[{}]: {}".format(i, id(sub_list)))for j, elem in enumerate(sub_list):print("\t\t[{}]: {}".format(j, id(elem)))

您将获得以下输出:

x: 1y: [1]z: [1, 1, 1, 1]my_list:[0]: 4300763792[0]: 4298171528[1]: 4298171528[2]: 4298171528[3]: 4298171528[1]: 4300763792[0]: 4298171528[1]: 4298171528[2]: 4298171528[3]: 4298171528[2]: 4300763792[0]: 4298171528[1]: 4298171528[2]: 4298171528[3]: 4298171528

所以现在让我们一步一步来。你有x,它是1,还有一个包含x的单个元素列表y。你的第一步是y * 4,它会给你一个新列表z,基本上是[x, x, x, x],即它创建一个新列表,其中包含4个元素,这些元素是对初始x对象的引用。下一步非常相似。你基本上做z * 3,这是[[x, x, x, x]] * 3并返回10,原因与第一步相同。

除了正确解释问题的已接受答案之外,而不是使用以下代码创建包含重复元素的列表:

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

此外,您可以使用itertools.repeat()创建重复元素的迭代器对象:

>>> a = list(repeat(1,4))[1, 1, 1, 1]>>> a[0] = 5>>> a[5, 1, 1, 1]

附注:如果您使用NumPy并且只想创建一个1或0数组,您可以使用np.onesnp.zeros和/或其他数字使用np.repeat

>>> import numpy as np>>> np.ones(4)array([1., 1., 1., 1.])>>> np.ones((4, 2))array([[1., 1.],[1., 1.],[1., 1.],[1., 1.]])>>> np.zeros((4, 2))array([[0., 0.],[0., 0.],[0., 0.],[0., 0.]])>>> np.repeat([7], 10)array([7, 7, 7, 7, 7, 7, 7, 7, 7, 7])

Python容器包含对其他对象的引用。参见此示例:

>>> a = []>>> b = [a]>>> b[[]]>>> a.append(1)>>> b[[1]]

在这个b中是一个列表,它包含一个对列表a的引用。列表a是可变的。

列表乘以整数相当于将列表多次添加到自身(参见公共序列操作)。继续示例:

>>> c = b + b>>> c[[1], [1]]>>>>>> a[0] = 2>>> c[[2], [2]]

我们可以看到列表c现在包含对列表a的两个引用,这相当于c = b * 2

Python FAQ还包含对此行为的解释:如何创建多维列表?

每个人都在解释正在发生的事情。我会提出一个解决它的方法:

my_list = [[1 for i in range(4)] for j in range(3)]
my_list[0][0] = 5print(my_list)

然后你得到:

[[5, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]

简单地说,这是因为在python中一切正常引用,所以当你以这种方式创建列表时,你基本上会遇到这样的问题。

要解决您的问题,您可以执行以下任一操作:1.使用numpy数组留档numpy.empty2.将列表附加到列表中。3.如果你愿意,你也可以使用字典

通过使用内置列表函数,您可以这样做

aout:[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]#Displaying the list
a.remove(a[0])out:[[1, 1, 1, 1], [1, 1, 1, 1]]# Removed the first element of the list in which you want altered number
a.append([5,1,1,1])out:[[1, 1, 1, 1], [1, 1, 1, 1], [5, 1, 1, 1]]# append the element in the list but the appended element as you can see is appended in last but you want that in starting
a.reverse()out:[[5, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]#So at last reverse the whole list to get the desired list

试图更具描述性地解释它,

操作1:

x = [[0, 0], [0, 0]]print(type(x)) # <class 'list'>print(x) # [[0, 0], [0, 0]]
x[0][0] = 1print(x) # [[1, 0], [0, 0]]

操作2:

y = [[0] * 2] * 2print(type(y)) # <class 'list'>print(y) # [[0, 0], [0, 0]]
y[0][0] = 1print(y) # [[1, 0], [1, 0]]

注意到为什么不修改第一个列表的第一个元素不修改每个列表的第二个元素?那是因为[0] * 2实际上是一个由两个数字组成的列表,对0的引用不能修改。

如果您想创建克隆副本,请尝试操作3:

import copyy = [0] * 2print(y)   # [0, 0]
y = [y, copy.deepcopy(y)]print(y) # [[0, 0], [0, 0]]
y[0][0] = 1print(y) # [[1, 0], [0, 0]]

另一种创建克隆副本的有趣方法,操作4:

import copyy = [0] * 2print(y) # [0, 0]
y = [copy.deepcopy(y) for num in range(1,5)]print(y) # [[0, 0], [0, 0], [0, 0], [0, 0]]
y[0][0] = 5print(y) # [[5, 0], [0, 0], [0, 0], [0, 0]]

my_list = [[1]*4] * 3在内存中创建一个列表对象[1,1,1,1]并复制其引用3次。这相当于obj = [1,1,1,1]; my_list = [obj]*3。对obj的任何修改都将反映在列表中引用obj的三个地方。正确的说法是:

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

my_list = [[1 for __ in range(4)] for _ in range(3)]

这里要注意的重要事情*运算符主要是用于创建文字列表。尽管1是不可变的,但obj = [1]*4仍然会创建一个重复4次的1列表以形成[1,1,1,1]。但是如果对不可变对象进行了任何引用,该对象将被新对象覆盖。

这意味着如果我们做obj[1] = 42,那么obj将成为[1,42,1,1]没有[42,42,42,42],正如一些人可能假设的那样。这也可以验证:

>>> my_list = [1]*4>>> my_list[1, 1, 1, 1]
>>> id(my_list[0])4522139440>>> id(my_list[1])  # Same as my_list[0]4522139440

>>> my_list[1] = 42  # Since my_list[1] is immutable, this operation overwrites my_list[1] with a new object changing its id.>>> my_list[1, 42, 1, 1]
>>> id(my_list[0])4522139440>>> id(my_list[1])  # id changed4522140752>>> id(my_list[2])  # id still same as my_list[0], still referring to value `1`.4522139440

@spelchekr从Python列表乘法:[[…]]*3使3个列表在修改时相互镜像和我有同样的问题“为什么只有外部*3创建更多引用,而内部没有?为什么不是都是1?”

li = [0] * 3print([id(v) for v in li])  # [140724141863728, 140724141863728, 140724141863728]li[0] = 1print([id(v) for v in li])  # [140724141863760, 140724141863728, 140724141863728]print(id(0))  # 140724141863728print(id(1))  # 140724141863760print(li)     # [1, 0, 0]
ma = [[0]*3] * 3  # mainly discuss inner & outer *3 hereprint([id(li) for li in ma])  # [1987013355080, 1987013355080, 1987013355080]ma[0][0] = 1print([id(li) for li in ma])  # [1987013355080, 1987013355080, 1987013355080]print(ma)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

这是我在尝试上面的代码后的解释:

  • 内部*3也创建引用,但它的引用是不可变的,就像[&0, &0, &0]一样,那么当你改变li[0]时,你不能改变const int0的任何底层引用,所以你可以把引用地址改成新的&1
  • ma = [&li, &li, &li]li是可变的,所以当你调用ma[0][0] = 1时,ma[0][0]等于&li[0],所以所有&li实例都会将其第一个地址更改为&1

我添加我的答案以图解方式解释相同的问题。

你创建2D的方式创建了一个浅列表

arr = [[0]*cols]*row

相反,如果您想更新列表的元素,您应该使用

rows, cols = (5, 5)arr = [[0 for i in range(cols)] for j in range(rows)]

补充说明

可以使用以下方式创建列表:

arr = [0]*N

arr = [0 for i in range(N)]

在第一种情况下,数组的所有索引都指向同一个整数对象

输入图片描述

当您将值分配给特定索引时,会创建一个新的int对象,例如arr[4] = 5创建

输入图片描述

现在让我们看看当我们创建一个列表时会发生什么,在这种情况下,我们顶部列表的所有元素都将指向同一个列表

输入图片描述

如果你更新任何索引的值,都会创建一个新的int对象。但由于所有顶级列表索引都指向同一个列表,所有行看起来都一样。你会觉得更新一个元素就是更新该列中的所有元素。

输入图片描述

学分:感谢Pranav Devarakonda简单的解释这里

我来到这里是因为我正在寻找如何嵌套任意数量的列表。上面有很多解释和具体示例,但是您可以使用以下递归函数推广…的列表的N维列表:

import copy
def list_ndim(dim, el=None, init=None):if init is None:init = el
if len(dim)> 1:return list_ndim(dim[0:-1], None, [copy.copy(init) for x in range(dim[-1])])
return [copy.deepcopy(init) for x in range(dim[0])]

你第一次调用函数是这样的:

dim = (3,5,2)el = 1.0l = list_ndim(dim, el)

其中(3,5,2)是结构维度的元组(类似于numpyshape参数),1.0是您希望结构初始化的元素(也适用于无)。请注意,init参数仅由递归调用提供以转发嵌套子列表

上面的输出:

[[[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]],[[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]],[[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]]]

设置具体要素:

l[1][3][1] = 56l[2][2][0] = 36.0+0.0jl[0][1][0] = 'abc'

结果输出:

[[[1.0, 1.0], ['abc', 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]],[[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 56.0], [1.0, 1.0]],[[1.0, 1.0], [1.0, 1.0], [(36+0j), 1.0], [1.0, 1.0], [1.0, 1.0]]]

列表的非类型化性质如上所示

请注意,序列中的项目不会被复制;它们被多次引用。这经常困扰着新的Python程序员;考虑:

>>> lists = [[]] * 3>>> lists[[], [], []]>>> lists[0].append(3)>>> lists[[3], [3], [3]]

发生的事情是[[]]是一个包含空列表的单元素列表,因此[[]] * 3的所有三个元素都是对这个空列表的引用。修改列表的任何元素都会修改这个单一列表。

另一个解释这一点的例子是使用多维数组

您可能尝试创建一个像这样的多维数组:

>>> A = [[None] * 2] * 3

如果你打印它,这看起来是正确的:

>>> A[[None, None], [None, None], [None, None]]

但是当你分配一个值时,它会出现在多个地方:

>>> A[0][0] = 5>>> A[[5, None], [5, None], [5, None]]

原因是使用*复制列表不会创建副本,它只会创建对现有对象的引用。3创建一个列表,其中包含对长度为2的同一列表的3个引用。对一行的更改将显示在所有行中,这几乎肯定不是您想要的。

虽然最初的问题使用乘法运算符构造了子列表,但我将添加一个使用相同列表作为子列表的示例。添加此答案是为了完整性,因为此问题通常用作问题的规范

node_count = 4colors = [0,1,2,3]sol_dict = {node:colors for node in range(0,node_count)}

列表中的每个字典值是相同的对象,试图改变其中一个字典值将在所有字典中看到。

>>> sol_dict{0: [0, 1, 2, 3], 1: [0, 1, 2, 3], 2: [0, 1, 2, 3], 3: [0, 1, 2, 3]}>>> [v is colors for v in sol_dict.values()][True, True, True, True]>>> sol_dict[0].remove(1)>>> sol_dict{0: [0, 2, 3], 1: [0, 2, 3], 2: [0, 2, 3], 3: [0, 2, 3]}

构造字典的正确方法是为每个值使用列表的副本。

>>> colors = [0,1,2,3]>>> sol_dict = {node:colors[:] for node in range(0,node_count)}>>> sol_dict{0: [0, 1, 2, 3], 1: [0, 1, 2, 3], 2: [0, 1, 2, 3], 3: [0, 1, 2, 3]}>>> sol_dict[0].remove(1)>>> sol_dict{0: [0, 2, 3], 1: [0, 1, 2, 3], 2: [0, 1, 2, 3], 3: [0, 1, 2, 3]}