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

Posted

技术标签:

【中文标题】列表更改意外地反映在子列表中【英文标题】:List of lists changes reflected across sublists unexpectedly 【发布时间】:2022-01-21 16:35:22 【问题描述】:

我需要在 Python 中创建一个列表列表,所以我输入了以下内容:

my_list = [[1] * 4] * 3

列表如下所示:

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

然后我更改了最里面的值之一:

my_list[0][0] = 5

现在我的列表如下所示:

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

这不是我想要或期望的。有人可以解释发生了什么,以及如何解决它吗?

【问题讨论】:

请注意,同样的逻辑也适用于 dicts 列表,因为可变对象的别名存在同样的基本问题。有关更具体的问题,请参阅 ***.com/questions/46835197/…。 【参考方案1】:

当您编写[x]*3 时,您基本上会得到[x, x, x] 列表。也就是说,一个包含 3 个对同一 x 的引用的列表。然后,当您修改此单个 x 时,可以通过对它的所有三个引用看到它:

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

x[0] = 42
print(f"x: x")
# x: [42, 1, 1, 1]
print(f"l: l")
# l: [[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 元素列表 [[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。

【讨论】:

我很惊讶没有人指出这一点,这里的答案具有误导性。 [x]*3 存储 3 个引用,如 [x, x, x] 仅在 x 可变时才正确。这不适用于例如a=[4]*3,在a[0]=5之后,a=[5,4,4]. 从技术上讲,它仍然是正确的。 [4]*3 本质上等同于 x = 4; [x, x, x]。不过,这确实不会导致任何问题,因为4 是不可变的。另外,您的另一个示例并不是真正的不同情况。即使x 是可变的,a = [x]*3; a[0] = 5 也不会引起问题,因为您没有修改x,只修改a。我不会将我的回答描述为具有误导性或不正确 - 如果您正在处理不可变的对象,您只是不能在脚上开枪。 @Allanqunzi 你错了。执行x = 1000; lst = [x]*2; lst[0] is lst[1] -> True。 Python 在这里不区分可变对象和不可变对象。 @all,很抱歉这么晚才讨论这个话题。但有一点很重要,所以即使我有一些列表,其中一个元素是另一个元素的引用,那么使用列表可能根本不安全!【参考方案2】:
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for _ in range(size)]

Live visualization 使用 Python 导师:

【讨论】:

那么,为什么如果我们写 matrix= [[x] * 2] 不会像您描述的示例那样为同一个对象生成 2 个元素,这似乎是同一个概念,我是什么不见了? @AhmedMohamed 实际上,它确实创建了一个列表,其中包含 x 所指的完全相同对象的两个元素。如果您使用 x = object() 创建一个全局唯一的对象,然后创建 matrix = [[x] * 2] 这些确实会实现:matrix[0][0] is matrix[0][1] @nadrimajstor 那么为什么矩阵[0] 的变化不会影响矩阵[1],就像上面的二维矩阵示例一样。 @AhmedMohamed 当您制作可变序列的“副本”时会出现惊喜(在我们的示例中,它是 list),所以如果 row = [x] * 2matrix = [row] * 2 两行都是同一个对象,现在变为一行matrix[0][0] = y 突然反映在另一行(matrix[0][0] is matrix[1][0]) == True @AhmedMohamed 看看Ned Batchelder - Facts and Myths about Python names and values,它可能会提供更好的解释。 :)【参考方案3】:

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

你写

lst = [[1] * 4] * 3

这相当于:

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

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

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

因为lst[0] 只不过是lst1

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

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

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

【讨论】:

只是对这里好答案的一个小补充:很明显,如果您执行id(lst[0][0])id(lst[1][0]) 甚至id(lst[0])id(lst[1]),那么您正在处理同一个对象【参考方案4】:
[[1] * 4] * 3

甚至:

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

创建一个引用内部[1,1,1,1] 3 次的列表 - 而不是内部列表的三个副本,因此无论何时修改列表(在任何位置),您都会看到更改 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]]

这可能不那么令人惊讶。

【讨论】:

您可以使用“is”运算符来发现这一点。 ls[0] 是 ls[1] 返回 True。【参考方案5】:

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 仍将创建一个1 列表,重复4 次以形成[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 changed
4522140752
>>> id(my_list[2])  # id still same as my_list[0], still referring to value `1`.
4522139440

【讨论】:

这与文字无关。 obj[2] = 42 替换索引2 处的引用,而不是改变该索引引用的对象,这是myList[2][0] = ... 所做的(myList[2] 是一个列表,并且分配改变了在 tha 列表中的索引 0 处引用)。当然,整数不是可变的,但是很多对象类型的。请注意,[....] 列表显示符号也是文字语法的一种形式!不要将复合对象(例如列表)和标量对象(例如整数)与可变对象和不可变对象混淆。【参考方案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]

附:如果您使用 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])

【讨论】:

【参考方案7】:

Python 容器包含对其他对象的引用。看这个例子:

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

在此b 中是一个列表,其中包含一个对列表a 的引用的项目。列表a 是可变的。

列表与整数相乘相当于将列表与自身相加多次(参见common sequence operations)。所以继续这个例子:

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

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

Python FAQ 还包含对此行为的解释:How do I create a multidimensional list?

【讨论】:

【参考方案8】:

让我们用以下方式重写你的代码:

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

my_list = [z] * 3

然后,运行以下代码以使一切更清楚。代码所做的基本上就是打印获取到的对象的ids,即

返回[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: 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

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

【讨论】:

【参考方案9】:

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

要解决您的问题,您可以执行以下任一操作: 1.使用numpy数组documentation for numpy.empty 2. 获得列表后追加列表。 3.如果你愿意,你也可以使用字典

【讨论】:

【参考方案10】:

这些问题有很多答案,我添加我的答案以图解方式解释。

您创建 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 的简单解释here

【讨论】:

【参考方案11】:

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

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

【讨论】:

【参考方案12】:

来自Python list multiplication: [[...]]*3 makes 3 lists which mirror each other when modified 的@spelchekr 我也有同样的问题 “为什么只有外部的 *3 会创建更多的引用而内部的不会?为什么不都是 1?”

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

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

内部*3也创建了引用,但是它的引用是不可变的,比如[&0, &0, &0],那么当你改变li[0]时,你就不能改变const int 0的任何底层引用,所以你可以只需将参考地址更改为新的&1; 虽然ma = [&li, &li, &li]li是可变的,所以当你调用ma[0][0] = 1时,ma[0][0]等于&li[0],所以所有&li实例都会将它的第一个地址更改为&1

【讨论】:

【参考方案13】:

试图更描述性地解释它,

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

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

如果您想创建克隆副本,请尝试操作 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]]

【讨论】:

【参考方案14】:

通过使用内置列表功能,您可以这样做

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

【讨论】:

注意,如果进行第二步,第四步可以省略:a.insert(0,[5,1,1,1])【参考方案15】:

我来到这里是因为我想看看如何嵌套任意数量的列表。上面有很多解释和具体的例子,但是你可以用下面的递归函数来泛化N维list of lists的list:

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)

其中(3,5,2) 是结构维度的元组(类似于numpy shape 参数),1.0 是您希望用于初始化结构的元素(也适用于None)。请注意,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] = 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]]]

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

【讨论】:

【参考方案16】:

虽然原始问题使用乘法运算符构造了 sublists,但我将添加一个使用 same 列表作为子列表的示例。添加此答案以确保完整性,因为此问题通常用作该问题的规范

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]

【讨论】:

【参考方案17】:

注意,序列中的项目不会被复制;它们被多次引用。这常常困扰着新的 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 创建一个列表,其中包含 3 个对长度为 2 的相同列表的引用。对一行的更改将显示在所有行中,这几乎肯定不是您想要的。

【讨论】:

以上是关于列表更改意外地反映在子列表中的主要内容,如果未能解决你的问题,请参考以下文章

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

如何以编程方式在自定义列表视图中进行更改以立即反映?

滚动列表时视图意外更改

更改函数列表时出现意外结果(lambda)[重复]

创建和管理STL列表的子列表

通过子分支的不同提交在主分支上添加恢复的更改不会反映在主分支上