为什么使用乘法运算符对列表操作会创建指针列表?

25 投票
3 回答
10333 浏览
提问于 2025-04-17 10:01
>>> rows = [['']*5]*5
>>> rows
[['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', '']]
>>> rows[0][0] = 'x'

我本来期待行会变成:

[['x', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', ''], ['', '', '', '', '']]

结果却是:

[['x', '', '', '', ''], ['x', '', '', '', ''], ['x', '', '', '', ''], ['x', '', '', '', ''], ['x', '', '', '', '']]

看起来行列表中的元素都是指向同一个旧的 ['']*5 列表的指针。为什么会这样?这是Python的特性吗?

3 个回答

5

你说得对,Python在内部确实使用了指针,这是一种特性。我不太确定他们为什么选择这样做——我猜是为了提高速度和减少内存使用。

顺便提一下,这个问题就是为什么理解浅拷贝和深拷贝之间的区别非常重要。

8

在Python中,名字、函数参数和容器的引用语义是一个非常基本的设计决定。这影响了Python在很多方面的工作方式,而你提到的只是其中一个方面。在很多情况下,引用语义更方便,而在其他情况下,复制可能会更方便。在Python中,如果需要,你总是可以明确地创建一个副本,或者在这种情况下,使用列表推导式来代替:

rows = [[''] * 5 for i in range(5)]

你可以设计一种具有不同语义的编程语言,实际上有很多语言的语义是不同的,也有一些语言的语义是相似的。至于为什么做出这样的决定,这个问题有点难回答——一门语言总得有一些语义,你总是可以问为什么。你也可以问为什么Python是动态类型的,最终的答案就是这是Guido在1989年时做出的决定。

27

这种行为并不是只有在重复操作符(*)中才会出现。比如,当你用 + 把两个列表连接起来时,行为也是一样的:

In [1]: a = [[1]]

In [2]: b = a + a

In [3]: b
Out[3]: [[1], [1]]

In [4]: b[0][0] = 10

In [5]: b
Out[5]: [[10], [10]]

这和列表是对象有关,而对象是通过引用来存储的。当你使用 * 等操作时,实际上是重复了引用,因此你看到的行为就是这样。

下面的例子展示了 rows 中的所有元素都有相同的身份(也就是在 CPython 中的内存地址):

In [6]: rows = [['']*5]*5

In [7]: for row in rows:
   ...:     print id(row)
   ...:     
   ...:     
15975992
15975992
15975992
15975992
15975992

下面的例子和你的例子是等价的,只不过它为每一行创建了五个不同的列表:

rows = [['']*5 for i in range(5)]

撰写回答