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

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 &g...

问题描述:

我创建了一个列表:

>>> 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.onesandnp.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 * 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:

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

要解决您的问题,您可以执行以下任一操作:

  1. 使用 numpy 数组;numpy.empty 文档

  2. 当您获得列表时附加该列表。

  3. 你也可以使用字典

解决方案 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] = 1ma[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的开发者。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用