提问人:Charles Anderson 提问时间:10/27/2008 最后编辑:wjandreaCharles Anderson 更新时间:2/20/2023 访问量:77506
跨子列表意外反映的列表更改
List of lists changes reflected across sublists unexpectedly
问:
我创建了一个列表列表:
>>> 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 中初始化空列表字典?对于列表字典的类似问题
答:
[[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 个引用的 .当您修改此单曲时,它可以通过对它的所有三个引用来显示:[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 个列表进行 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
评论
[x]*3
[x, x, x]
x
a=[4]*3
a[0]=5
a=[5,4,4].
[4]*3
x = 4; [x, x, x]
4
a = [x]*3; a[0] = 5
x
x
a
x = 1000; lst = [x]*2; lst[0] is lst[1]
True
实际上,这正是您所期望的。让我们分解一下这里发生的事情:
你写
lst = [[1] * 4] * 3
这相当于:
lst1 = [1]*4
lst = [lst1]*3
这意味着是一个包含 3 个元素的列表,所有元素都指向 。这意味着以下两行是等效的:lst
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])
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for _ in range(size)]
使用 Python Tutor 进行实时可视化:
评论
x
x = object()
matrix = [[x] * 2]
matrix[0][0] is matrix[0][1]
list
row = [x] * 2
matrix = [row] * 2
matrix[0][0] = y
(matrix[0][0] is matrix[1][0]) == True
让我们按以下方式重写代码:
x = 1
y = [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: 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 个元素,这些元素是对初始对象的引用。下一步非常相似。你基本上做 ,这是 和 返回 ,原因与第一步相同。x
1
y
x
y * 4
z
[x, x, x, x]
x
z * 3
[[x, x, x, x]] * 3
[[x, x, x, x], [x, x, x, x], [x, x, x, x]]
除了正确解释问题的已接受答案外,而不是使用以下代码创建包含重复元素的列表:
[[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])
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]]
我们可以看到,列表现在包含两个对 list 的引用,相当于 .c
a
c = b * 2
Python FAQ 还包含对此行为的解释:如何创建多维列表?
每个人都在解释正在发生的事情。我会建议一种方法来解决它:
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]]
简单来说,这种情况正在发生,因为在 python 中,一切都通过引用工作,所以当你以这种方式创建列表列表时,你基本上会遇到这样的问题。
要解决您的问题,您可以执行其中一项操作:
- 使用 numpy 数组;numpy.empty 文档
- 在进入列表时追加列表。
- 如果需要,也可以使用字典
通过使用内置的列表函数,您可以这样做
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])
试图更描述性地解释它,
操作 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]]
my_list = [[1]*4] * 3
在内存中创建一个列表对象,并将其引用复制 3 次。这相当于 。任何修改都将反映在列表中引用的三个位置。
正确的陈述是:[1,1,1,1]
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)]
这里需要注意的重要一点是,运算符主要用于创建文本列表。虽然是不可变的,但仍然会创建一个重复 4 次的列表来形成。但是,如果对不可变对象进行了任何引用,则该对象将被新对象覆盖。*
1
obj = [1]*4
1
[1,1,1,1]
这意味着如果我们这样做,那么将变得不像某些人所假设的那样[42,42,42,42这也可以验证:]。
obj[1] = 42
obj
[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
评论
obj[2] = 42
2
myList[2][0] = ...
myList[2]
[....]
从 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]
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 对象,例如 createsarr[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.0
l = list_ndim(dim, el)
其中 是结构维度的元组(类似于 numpy 参数),并且是您希望用于初始化结构的元素(也适用于 None)。请注意,该参数仅由递归调用提供,以前移嵌套子列表(3,5,2)
shape
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] = 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]]]
上面演示了列表的非类型化特性
请注意,序列中的项不会被复制;它们会被多次引用。这经常困扰着新的 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 = 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]}
评论
.append