列表的更改意外反映在子列表中
- 2024-11-15 08:36:00
- admin 原创
- 16
问题描述:
我创建了一个列表:
>>> 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]]
我预计这只会影响第一个子列表,而不是所有子列表。即:
>>> print(xs)
[[5, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
为什么每个子列表的第一个元素都更改为5
?
参见:
如何克隆列表以使其在分配后不会意外更改?
如何在 Python 中初始化二维数组(如果不使用 NumPy,则为列表列表)?以获取问题的解决方法
对于字典列表的类似问题,字典列表在每次迭代中仅存储最后附加的值
如何在 Python 中初始化一个空列表字典?对于列表字典的类似问题
解决方案 1:
当你写入时[x]*3
,你实际上得到的是列表[x, x, x]
。也就是说,列表包含对同一个的 3 个引用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])}
"
f"id(xs[1]): {id(xs[1])}
"
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 个列表进行 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 的操作。
解决方案 2:
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for _ in range(size)]
使用Python Tutor进行实时可视化:
解决方案 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
,从而得到不同的列表。
解决方案 4:
[[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]]
这也许就不那么令人惊讶了。
解决方案 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
仍然会创建一个重复 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 changed
4522140752
>>> id(my_list[2]) # id still same as my_list[0], still referring to value `1`.
4522139440
解决方案 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]
PS 如果您正在使用 NumPy,并且只想创建一个由 1 或 0 组成的数组,则可以使用np.ones
andnp.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
是可变的。
将列表与整数相乘等效于将列表多次添加到自身(请参阅常见序列运算)。继续看这个例子:
>>> c = b + b
>>> c
[[1], [1]]
>>>
>>> a[0] = 2
>>> c
[[2], [2]]
我们可以看到列表c
现在包含两个对列表的引用a
,这相当于c = b * 2
。
Python FAQ 也包含对此行为的解释:如何创建多维列表?
解决方案 8:
我正在添加我的答案来以图表方式解释同样的事情。
你创建 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 的简单解释
解决方案 9:
让我们按以下方式重写您的代码:
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(" [{}]: {}".format(i, id(sub_list)))
for j, elem in enumerate(sub_list):
print(" [{}]: {}".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
单个元素列表。您的第一步是 ,这将为您带来一个新列表,即,即它创建一个新列表,该列表将包含 4 个元素,这些元素是对初始对象的引用。下一步非常相似。您基本上执行,即 并返回,原因与第一步相同。y
`xy * 4
z[x, x, x, x]
xz * 3
[[x, x, x, x]] * 3`[[x, x, x, x], [x, x, x, x], [x, x, x, x]]
解决方案 10:
简单地说,发生这种情况是因为在 python 中一切都是通过引用来工作的,所以当你以这种方式创建列表列表时,你基本上会遇到这样的问题。
要解决您的问题,您可以执行以下任一操作:
使用 numpy 数组;numpy.empty 文档
当您获得列表时附加该列表。
你也可以使用字典
解决方案 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:
@spelchekr 来自Python 列表乘法:[[...]]*3 生成 3 个列表,它们在修改时彼此镜像,我也有同样的问题“为什么只有外部列表*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
解决方案 15:
来自官方文件:
请注意,序列s中的项 不会被复制;它们会被多次引用。这经常困扰新的 Python 程序员;请考虑:
>>> lists = [[]] * 3 >>> lists [[], [], []] >>> lists[0].append(3) >>> lists [[3], [3], [3]]
发生的事情是,
[[]]
是一个包含空列表的单元素列表,因此的所有三个元素[[]] * 3
都是对这个单个空列表的引用。修改的任何元素都会lists
修改这个单个列表。您可以通过以下方式创建不同列表的列表:>>> lists = [[] for i in range(3)] >>> lists[0].append(3) >>> lists[1].append(5) >>> lists[2].append(7) >>> lists [[3], [5], [7]]
解决方案 16:
我来到这里是因为我想看看如何嵌套任意数量的列表。上面有很多解释和具体的例子,但你可以用以下递归函数概括 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)
其中(3,5,2)
是结构维度的元组(类似于 numpyshape
参数), 是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]]]
列表的非类型化本质已在上文中得到证明
解决方案 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]}
解决方案 18:
请注意,序列中的项目不会被复制;它们会被多次引用。这经常困扰新的 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 个引用的列表。对一行的更改将显示在所有行中,这几乎肯定不是您想要的。
解决方案 19:
每个子列表的第一个元素都更改为 5,因为“xs”有三次相同的列表,数据正在共享。打印不会显示这一点,但使用:
https://pypi.org/project/memory-graph/
你可以绘制数据图表并轻松看到以下内容:
import memory_graph # see install instructions at link above
xs = [[1] * 4] * 3
print(xs)
xs[0][0] = 5
print(xs)
memory_graph.d() # draw graph
相反,您可能希望使用列表推导,以便数据不被共享:
import memory_graph
xs = [[1] * 4 for _ in range(3)] # <----- list comprehension
print(xs)
xs[0][0] = 5
print(xs)
memory_graph.d()
输出结果如下:
[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
[[5, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
全面披露:我是memory_graph的开发者。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件