跨子列表意外反映的列表更改

List of lists changes reflected across sublists unexpectedly

提问人:Charles Anderson 提问时间:10/27/2008 最后编辑:wjandreaCharles Anderson 更新时间:2/20/2023 访问量:77506

问:

我创建了一个列表列表:

>>> 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 list nested-lists 可变

评论

0赞 Karl Knechtel 12/17/2021
请注意,同样的逻辑也适用于字典列表,因为对可变对象进行别名处理也存在相同的基本问题。有关更具体的问题,请参阅 stackoverflow.com/questions/46835197/...
1赞 Karl Knechtel 5/3/2022
当以其他方式创建列表列表时(但有相同的问题),是否有更具体的问题?例如,通过循环使用?.append
0赞 Karl Knechtel 6/4/2022
另请参阅 stackoverflow.com/questions/2612802,了解侧重于避免事后出现这种混叠的问题。
1赞 CrazyChucky 6/22/2022
相关新闻: nedbatchelder.com/text/names.html
0赞 ospider 11/17/2023
要在 Python 中使用矩阵,只需转到 numpy。列表列表确实很麻烦和缓慢。

答:

49赞 Blair Conrad 10/27/2008 #1
[[1] * 4] * 3

甚至:

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

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

它与以下示例相同:

>>> 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]]

这可能不那么令人惊讶。

评论

3赞 mipadi 10/27/2008
您可以使用“is”运算符来发现这一点。ls[0] is ls[1] 返回 True。
803赞 CAdaker 10/27/2008 #2

当你写的时候,你得到的基本上是列表。也就是说,一个列表包含对同一 3 个引用的 .当您修改此单曲时,它可以通过对它的所有三个引用来显示:[x]*3[x, x, x]xx

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 个列表进行 3 次引用。[1]*4


你可能想知道为什么不能像列表推导那样制作独立的对象。这是因为乘法运算符对对象进行操作,而不查看表达式。当您用于乘以 3 时,只会看到计算结果为 1 元素列表,而不是表达式文本。 不知道如何复制该元素,不知道如何重新评估,甚至不知道你想要副本,一般来说,甚至可能没有办法复制该元素。***[[1] * 4]*[[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 变成 2。[1] * 4[1]1.value = 2

评论

45赞 Allanqunzi 5/22/2015
令我惊讶的是,没有人指出这一点,这里的答案具有误导性。 存储 3 个引用,例如只有在可变时才正确。这不适用于例如,在 , 之后 ,[x]*3[x, x, x]xa=[4]*3a[0]=5a=[5,4,4].
69赞 CAdaker 5/22/2015
从技术上讲,它仍然是正确的。 实质上等同于。不过,这确实不会引起任何问题,因为它是不可变的。此外,您的另一个示例并不是一个不同的案例。 即使可变也不会引起问题,因为你不是在修改,只是在修改。我不会将我的答案描述为误导或不正确 - 如果你正在处理不可变的对象,你就不能搬起石头砸自己的脚。[4]*3x = 4; [x, x, x]4a = [x]*3; a[0] = 5xxa
34赞 timgeb 4/18/2016
@Allanqunzi你错了。做 -> 。Python 在这里没有区分可变对象和不可变对象。x = 1000; lst = [x]*2; lst[0] is lst[1]True
1赞 Lei Yang 3/31/2022
任何人都可以在 docs.python.org 中找到有关操作员的文件吗?我试过了,但 cna没有找到。*
0赞 Jasmijn 5/22/2022
@LeiYang 它列在“常见序列操作”下
74赞 PierreBdR 10/27/2008 #3

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

你写

lst = [[1] * 4] * 3

这相当于:

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

这意味着是一个包含 3 个元素的列表,所有元素都指向 。这意味着以下两行是等效的:lstlst1

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

只不过是.lst[0]lst1

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

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

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

评论

0赞 Sergiy Kolodyazhnyy 5/17/2017
只是对这里很好的答案的一个小补充:很明显,如果你这样做,或者甚至和id(lst[0][0])id(lst[1][0])id(lst[0])id(lst[1])
0赞 JobHunter69 6/11/2022
没有解释为什么修改一维列表会导致复制,而二维列表不会导致任何复制
1赞 Mark Ransom 11/5/2022
@JobHunter69复制也是如此;这都是幻觉。对于不可变对象(如数字或字符串)的一维列表,您无法更改这些列表元素;您只能将它们替换为新值。即使你没有,看起来你也有副本。2d 列表实际上是一个列表列表,您可以轻松更改这些内部列表。
183赞 nadrimajstor 8/27/2013 #4
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for _ in range(size)]

使用 Python Tutor 进行实时可视化

Frames and Objects

评论

1赞 Ahmed Mohamed 7/2/2017
那么,为什么如果我们写 matrix= [[x] * 2] 不像你描述的例子那样为同一个对象制作 2 个 elemnt,它似乎是同一个概念,我错过了什么?
1赞 nadrimajstor 7/2/2017
@AhmedMohamed 事实上,它确实制作了一个列表,其中包含引用的完全相同对象的两个元素。如果你用 然后使这些成为现实,那么创建一个全局唯一的对象:xx = object()matrix = [[x] * 2]matrix[0][0] is matrix[0][1]
0赞 Ahmed Mohamed 7/2/2017
@nadrimajstor为什么 matrix[0] 的变化不会像上面的 2D 矩阵示例那样影响 matrix[1]。
0赞 nadrimajstor 7/2/2017
@AhmedMohamed 当你制作可变序列的“副本”时,你会感到惊讶(在我们的例子中是一个),所以如果一个比一个,其中两行是完全相同的对象,现在对一行的更改突然反映在另一行中listrow = [x] * 2matrix = [row] * 2matrix[0][0] = y(matrix[0][0] is matrix[1][0]) == True
0赞 nadrimajstor 7/3/2017
@AhmedMohamed 看看 Ned Batchelder - 关于 Python 名称和值的事实和神话,因为它可能会提供更好的解释。:)
5赞 bagrat 6/10/2015 #5

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

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

my_list = [z] * 3

然后,运行以下代码以使所有内容更加清晰。代码所做的基本上是打印获取对象的 ids,这

返回对象的“标识”

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

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: 1
y: [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

所以现在让我们一步一步来。您有 which is ,以及一个包含 的元素列表。你的第一步是给你一个新列表,这基本上是 ,即它创建一个新列表,它将有 4 个元素,这些元素是对初始对象的引用。下一步非常相似。你基本上做 ,这是 和 返回 ,原因与第一步相同。x1yxy * 4z[x, x, x, x]xz * 3[[x, x, x, x]] * 3[[x, x, x, x], [x, x, x, x], [x, x, x, x]]

10赞 Mazdak 6/18/2015 #6

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

[[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]

P.S. 如果您使用的是 NumPy,并且只想创建一个 1 或 0 的数组,您可以使用 np.ones 和 np.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])
9赞 Zbyněk Winkler 4/6/2016 #7

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

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

这是一个列表,其中包含一个项目,该项目是对列表的引用。该列表是可变的。baa

列表乘以整数等同于将列表多次添加到自身中(请参阅常见的序列操作)。因此,继续这个例子:

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

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

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

4赞 awulll 4/24/2016 #8

每个人都在解释正在发生的事情。我会建议一种方法来解决它:

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

my_list[0][0] = 5
print(my_list)

然后你会得到:

[[5, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
5赞 Neeraj Komuravalli 6/14/2016 #9

简单来说,这种情况正在发生,因为在 python 中,一切都通过引用工作,所以当你以这种方式创建列表列表时,你基本上会遇到这样的问题。

要解决您的问题,您可以执行其中一项操作:

  1. 使用 numpy 数组;numpy.empty 文档
  2. 在进入列表时追加列表。
  3. 如果需要,也可以使用字典
2赞 Anand Tripathi 7/15/2016 #10

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

a
out:[[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

评论

2赞 U13-Forward 10/19/2018
请注意,如果您执行第二步,则可以删除第四步:a.insert(0,[5,1,1,1])
3赞 Adil Abbasi 8/10/2016 #11

试图更描述性地解释它,

操作 1:

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

x[0][0] = 1
print(x) # [[1, 0], [0, 0]]

操作 2:

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

y[0][0] = 1
print(y) # [[1, 0], [1, 0]]

注意到为什么修改第一个列表的第一个元素没有修改每个列表的第二个元素?那是因为 really 是两个数字的列表,并且无法修改对 0 的引用。[0] * 2

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

import copy
y = [0] * 2   
print(y)   # [0, 0]

y = [y, copy.deepcopy(y)]  
print(y) # [[0, 0], [0, 0]]

y[0][0] = 1
print(y) # [[1, 0], [0, 0]]

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

import copy
y = [0] * 2
print(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] = 5
print(y) # [[5, 0], [0, 0], [0, 0], [0, 0]]
13赞 jerrymouse 4/6/2017 #12

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

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

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

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

这意味着如果我们这样做,那么将变得不像某些人所假设的那样[42,42,42,42]。这也可以验证:obj[1] = 42obj[1,42,1,1]

>>> 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 changed
4522140752
>>> id(my_list[2])  # id still same as my_list[0], still referring to value `1`.
4522139440

评论

3赞 Martijn Pieters 7/25/2018
这与字面意思无关。 替换索引处的引用,而不是改变该索引引用的对象,这就是这样做的(是一个列表,并且同化会更改列表中索引 0 处的引用)。当然,整数是不可变的,但很多对象类型可变的。请注意,列表显示表示法也是文字语法的一种形式!不要将复合对象(如列表)和标量对象(如整数)与可变对象与不可变对象混淆。obj[2] = 422myList[2][0] = ...myList[2][....]
4赞 ouxiaogu 8/9/2019 #13

从 Python 列表乘法@spelchekr:[[...]]*3 制作了 3 个列表,这些列表在修改时相互镜像,我有同样的问题 “为什么只有外部的引用会创建更多的引用,而内部的引用却不会?为什么不都是 1?*3

li = [0] * 3
print([id(v) for v in li])  # [140724141863728, 140724141863728, 140724141863728]
li[0] = 1
print([id(v) for v in li])  # [140724141863760, 140724141863728, 140724141863728]
print(id(0))  # 140724141863728
print(id(1))  # 140724141863760
print(li)     # [1, 0, 0]

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

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

  • 内部也创建引用,但它的引用是不可变的,类似于 ,那么当你更改时,你不能更改 const int 的任何底层引用,所以你只能将引用地址更改为新的*3[&0, &0, &0]li[0]0&1;
  • while 和 是可变的,所以当你调用 时,等于 ,所以所有实例都会将其第一个地址更改为 。ma = [&li, &li, &li]lima[0][0] = 1ma[0][0]&li[0]&li&1
7赞 Deepak Patankar 6/21/2020 #14

我正在添加我的答案以图表方式解释相同的内容。

创建 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)] 

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

enter image description here

当您为特定索引赋值时,将创建一个新的 int 对象,例如 createsarr[4] = 5

enter image description here

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

enter image description here

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

enter image description here

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

0赞 Brian 10/23/2020 #15

我来到这里是因为我想看看如何嵌套任意数量的列表。上面有很多解释和具体的例子,但是你可以概括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.0
l = list_ndim(dim, el)

其中 是结构维度的元组(类似于 numpy 参数),并且是您希望用于初始化结构的元素(也适用于 None)。请注意,该参数仅由递归调用提供,以前移嵌套子列表(3,5,2)shape1.0init

以上输出:

[[[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] = 56
l[2][2][0] = 36.0+0.0j
l[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]]]

上面演示了列表的非类型化特性

0赞 shuberman 11/23/2020 #16

请注意,序列中的项不会被复制;它们会被多次引用。这经常困扰着新的 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 个引用。对一行的更改将显示在所有行中,这几乎可以肯定不是您想要的。*

0赞 wwii #17

虽然原始问题使用乘法运算符构造子列表,但我将添加一个对子列表使用相同列表的示例。为了完整起见,添加此答案,因为这个问题通常被用作该问题的规范

node_count = 4
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]}
>>> [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]}