Python3:为什么创建列表列表会产生意外行为?

时间:2017-07-04 23:54:57

标签: python list python-3.x

编辑:这个问题是关于为什么行为就是这样,而不是如何绕过它,这就是所谓的重复是关于。

我使用以下符号在不同情况下创建特定大小的列表。例如:

>>> [None] * 5
[None, None, None, None, None]
>>>

这似乎按预期工作,并且短于:

>>> [None for _ in range(5)]
[None, None, None, None, None]
>>>

然后我尝试使用相同的方法创建列表列表:

>>> [[]] * 5
[[], [], [], [], []]
>>>

足够公平。它似乎按预期工作。

然而,在通过调试器时,我注意到所有子列表桶具有相同的值,即使我只添加了单个项。例如:

>>> t = [[]] * 5
>>> t
[[], [], [], [], []]
>>> t[1].append(4)
>>> t
[[4], [4], [4], [4], [4]]
>>> t[0] is t[1]
True
>>>

我不希望所有顶级数组元素都引用单个子列表;我期待5个独立的子列表。

为此,我必须编写如下代码:

>>> t = [[] for _ in range(5)]
>>> t
[[], [], [], [], []]
>>> t[2].append(4)
>>> t
[[], [], [4], [], []]
>>> t[0] is t[1]
False
>>>

我明显遗漏了一些东西,可能是一个历史事实,或者只是一种不同的方式来观察这里的一致性。

有人可以解释为什么两个不同的代码片段,人们会合理地期望它们彼此等效,实际上最终会隐式地产生不同的和非显而易见的(IMO)结果,特别是考虑到Python'始终显式明显的

请注意,我已经知道this question,这与我提出的问题不同。

我只是在寻找详细的解释/理由。如果出现此行为的历史,技术和/或理论原因,请务必提供一两个参考。

2 个答案:

答案 0 :(得分:3)

执行以下操作时:

[[]]*n

首先创建列表,然后将*运算符与int n一起使用。这将获取列表中的任何对象,并创建n次重复。

但是在Python中,显式优于隐式,你不能隐式地复制这些对象。实际上,这与Python的语义一致。

尝试为Python 隐式制作副本的单个案例命名。

此外,它与列表中的添加一致:

l = [1, [], 'a']

l2 = l + l + l

l[1].append('foo')

print(l2)

输出:

[1, ['foo'], 'a', 1, ['foo'], 'a', 1, ['foo'], 'a']

现在,正如评论中所提到的,来自C ++的内容是有道理的,上面的内容会令人惊讶,但如果将其用于Python,那么上面就是期望的

另一方面:

[[] for _ in range(5)]

列表是否理解。它相当于:

lst = []
for _ in range(5):
    lst.append([])

在这里,很明显,每次进入循环时,都会创建一个新列表。这就是字面语法的工作原理。

顺便说一句,除了我喜欢的一个特定习语外,我几乎从不在列表上使用*运算符:

>>> x = list(range(1, 22))
>>> it_by_three = [iter(x)]*3
>>> for a,b,c in zip(*it_by_three):
...    print(a, b, c)
...
1 2 3
4 5 6
7 8 9
10 11 12
13 14 15
16 17 18
19 20 21

答案 1 :(得分:1)

对于cpython,源代码的相关部分位于listobject.c中的函数list_repeat中。下面重复了一个启发性片段,我添加了评论:

np = (PyListObject *) PyList_New(size);  // make a new PyListObject

/* some code omitted */

items = np->ob_item;          // grabs the list of pointers of the *new* object
if (Py_SIZE(a) == 1) {        // this is the case for a 1-element list being multiplied
    elem = a->ob_item[0];     // grabs the pointer of the element of the *original* object
    for (i = 0; i < n; i++) {
        items[i] = elem;      // assigns the original pointer to the new list
        Py_INCREF(elem);
    }
    return (PyObject *) np;
}

由于PyListObject主要是Vector,其中包含指向列表元素的指针列表,因此将这些点作为元素分配给新PyListObject很简单。

相反,想象代码是否需要复制位于每个指针的对象。它会更复杂,并且会有明显的性能影响。但是,我不会推测这个设计决定的动机。