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

Cha*_*son 576 python list mutable nested-lists

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

myList = [[1] * 4] * 3
Run Code Online (Sandbox Code Playgroud)

列表看起来像这样:

[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]  
Run Code Online (Sandbox Code Playgroud)

然后我改变了最里面的一个值:

myList[0][0] = 5
Run Code Online (Sandbox Code Playgroud)

现在我的列表看起来像这样:

[[5, 1, 1, 1], [5, 1, 1, 1], [5, 1, 1, 1]]  
Run Code Online (Sandbox Code Playgroud)

这不是我想要或期望的.有人可以解释一下发生了什么,以及如何解决这个问题?

CAd*_*ker 501

当你写作时,[x]*3你基本上得到了这个列表[x, x, x].也就是说,列表中有3个引用相同的列表x.然后,当您修改此单个时x,通过对它的所有三个引用都可以看到它.

要解决此问题,您需要确保在每个位置创建新列表.一种方法是

[[1]*4 for _ in range(3)]
Run Code Online (Sandbox Code Playgroud)

这将[1]*4每次重新评估,而不是评估一次,并对3个列表进行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.

  • 从技术上讲,它仍然是正确的.`[4]*3`基本上相当于`x = 4; [x,x,x]`.但是,这确实不会导致任何问题,因为`4`是不可变的.另外,你的另一个例子并非如此.`a = [x]*3; 即使`x`是可变的,a [0] = 5`也不会引起问题,因为你不修改`x`,只修改`a`.我不会将我的回答描述为误导或错误 - 如果你正在处理不可变对象,你只是*不能*自己在脚下射击. (37认同)
  • 我很惊讶没有人指出这一点,这里的答案是误导性的.`[x]*3`存储3个引用,如`[x,x,x]`仅在`x`可变时才正确.这不适用于例如`a = [4]*3`,其中'a [0] = 5`,`a = [5,4,4]. (21认同)
  • @Allanqunzi你错了.做`x = 1000; lst = [x]*2; lst [0]是lst [1]` - >`True`.Python无论如何都不区分可变对象和不可变对象. (17认同)

小智 118

size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for i in range(size)]
Run Code Online (Sandbox Code Playgroud)

框架和对象

Live Python Tutor Visualize


Pie*_*BdR 46

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

你写

lst = [[1] * 4] * 3
Run Code Online (Sandbox Code Playgroud)

这相当于:

lst1 = [1]*4
lst = [lst1]*3
Run Code Online (Sandbox Code Playgroud)

这意味着lst是一个包含3个元素的列表lst1.这意味着以下两行是等效的:

lst[0][0] = 5
lst1[0] = 5
Run Code Online (Sandbox Code Playgroud)

就像lst[0]什么一样lst1.

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

lst = [ [1]*4 for n in range(3) ] #python 3
lst = [ [1]*4 for n in xrange(3) ] #python 2
Run Code Online (Sandbox Code Playgroud)

在这种情况下,对每个n重新计算表达式,从而得到不同的列表.


Bla*_*rad 33

[[1] * 4] * 3
Run Code Online (Sandbox Code Playgroud)

甚至:

[[1, 1, 1, 1]] * 3
Run Code Online (Sandbox Code Playgroud)

创建一个引用内部[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]]
Run Code Online (Sandbox Code Playgroud)

它可能不那么令人惊讶.

  • 您可以使用"is"运算符来发现这一点.ls [0]是ls [1]返回True. (3认同)

jer*_*use 9

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)]
Run Code Online (Sandbox Code Playgroud)

或者

my_list = [[1 for __ in range(4)] for _ in range(3)]
Run Code Online (Sandbox Code Playgroud)

这里要注意的重要一点是,*运算符主要用于创建文字列表。虽然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
Run Code Online (Sandbox Code Playgroud)
>>> 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
Run Code Online (Sandbox Code Playgroud)

  • 这与文字无关。`obj[2] = 42` **替换索引 `2` 处的引用**,而不是改变该索引引用的对象,这就是 `myList[2][0] = ...` 所做的( `myList[2]` 是一个列表,赋值改变了列表中索引 0 处的引用)。当然,整数是不可变的,但是很多对象类型*是*。请注意,`[....]` 列表显示符号也是文字语法的一种形式!不要将复合对象(例如列表)和标量对象(例如整数)与可变对象与不可变对象混淆。 (3认同)

Zby*_*ler 7

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

>>> a = []
>>> b = [a]
>>> b
[[]]
>>> a.append(1)
>>> b
[[1]]
Run Code Online (Sandbox Code Playgroud)

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

列表乘以整数相当于将列表多次添加到自身(参见常见序列操作)。所以继续这个例子:

>>> c = b + b
>>> c
[[1], [1]]
>>>
>>> a[0] = 2
>>> c
[[2], [2]]
Run Code Online (Sandbox Code Playgroud)

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

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


Dee*_*kar 7

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

创建 2D 的方式创建了一个浅列表

arr = [[0]*cols]*row
Run Code Online (Sandbox Code Playgroud)

相反,如果您想更新列表的元素,您应该使用

rows, cols = (5, 5) 
arr = [[0 for i in range(cols)] for j in range(rows)] 
Run Code Online (Sandbox Code Playgroud)

解释

可以使用以下方法创建一个列表:

arr = [0]*N 
Run Code Online (Sandbox Code Playgroud)

或者

arr = [0 for i in range(N)] 
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述

当您为特定索引分配值时,会创建一个新的 int 对象,例如arr[4] = 5创建

在此输入图像描述

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

在此输入图像描述

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

在此输入图像描述

致谢:感谢Pranav Devarakonda简单解释


Kas*_*mvd 6

除了正确解释问题的接受答案之外,在列表理解中,如果使用python-2.x xrange(),则返回一个更有效的生成器(range()在python 3中执行相同的工作)_而不是一次性变量n:

[[1]*4 for _ in xrange(3)]      # and in python3 [[1]*4 for _ in range(3)]
Run Code Online (Sandbox Code Playgroud)

此外,作为更多Pythonic方式,您可以使用它itertools.repeat()来创建重复元素的迭代器对象:

>>> a=list(repeat(1,4))
[1, 1, 1, 1]
>>> a[0]=5
>>> a
[5, 1, 1, 1]
Run Code Online (Sandbox Code Playgroud)

PS使用numpy的,如果你只是想创建1或0,你可以使用数组np.onesnp.zeros和/或其他使用次数np.repeat():

In [1]: import numpy as np

In [2]: 

In [2]: np.ones(4)
Out[2]: array([ 1.,  1.,  1.,  1.])

In [3]: np.ones((4, 2))
Out[3]: 
array([[ 1.,  1.],
       [ 1.,  1.],
       [ 1.,  1.],
       [ 1.,  1.]])

In [4]: np.zeros((4, 2))
Out[4]: 
array([[ 0.,  0.],
       [ 0.,  0.],
       [ 0.,  0.],
       [ 0.,  0.]])

In [5]: np.repeat([7], 10)
Out[5]: array([7, 7, 7, 7, 7, 7, 7, 7, 7, 7])
Run Code Online (Sandbox Code Playgroud)


bag*_*rat 5

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

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

my_list = [z] * 3
Run Code Online (Sandbox Code Playgroud)

然后有了这个,运行以下代码以使一切更加清晰。代码所做的基本上是打印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)))
Run Code Online (Sandbox Code Playgroud)

您将获得以下输出:

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
Run Code Online (Sandbox Code Playgroud)

所以现在让我们一步一步来。你有xwhich is1和一个y包含x. 您的第一步是为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]],原因与第一步相同。


小智 5

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

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

  1. 使用numpy数组;numpy.empty 的文档
  2. 当您到达列表时附加该列表。
  3. 如果需要,您也可以使用字典