无限嵌套列表中到底发生了啥?

Posted

技术标签:

【中文标题】无限嵌套列表中到底发生了啥?【英文标题】:What's exactly happening in infinite nested lists?无限嵌套列表中到底发生了什么? 【发布时间】:2011-12-02 05:55:01 【问题描述】:

可以在 Python 中创建无限嵌套列表。这很清楚,虽然不受欢迎且绝对没有用,但这是一个众所周知的事实。

>>> a = [0]
>>> a[0] = a
>>> a
[[...]]
>>> a[0] == a
True

我的问题是,这里发生了什么:

>>> a = [0]
>>> b = [0]
>>> a[0], b[0] = b, a
>>> a
[[[...]]]
>>> b
[[[...]]]
>>> a == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp
>>> a[0] == b
True
>>> a[0][0] == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp
>>> a[0][0][0] == b
True
>>> 

每一种更深,当我试图理解它时,我感觉我的大脑更像是要爆炸了。我明白了,a 包含 b,包含 a 等等......

现在我关于这个的问题。我们这里真的有两个列表,还是只有一个?像这样的东西是如何存储在内存中的?让程序员实现这样奇怪的东西的目的是什么?

请不要把这个问题看得太严肃。别忘了,编程有时很有趣。

【问题讨论】:

很棒的问题。我真的很喜欢 Python 的这个特性,虽然我也从来没有发现它的用途。如果有人能想出这个功能的实际应用,那就太好了。或者编写一个模块来生成包含所有列表的列表:P @andronikus: xkcd.com/468 哈哈不错。哥德尔是个棘手的人! 【参考方案1】:

免责声明:我不使用 Python,所以我说的有些话可能是错误的。 Python 专家,请随时纠正我。

很好的问题。我认为主要的误解(如果我什至不能这样称呼它;你如何得出你使用的思维过程是完全合理的)你有这个提示你问这个问题是:

当我写b[0] = a 时,并不意味着a b。这意味着b 包含一个指向a 指向的事物的引用。

变量ab 本身甚至不是“事物”本身,它们本身也只是指向内存中其他匿名“事物”的指针。

引用的概念是非编程领域的一次重大飞跃,因此让我们牢记这一点来逐步完成您的程序:

>>> a = [0]

您创建了一个列表,其中恰好有一些东西(暂时忽略它)。重要的是它是一个列表。该列表存储在内存中。假设它存储在内存位置 1001。然后,赋值 = 创建一个变量 a,编程语言允许您稍后使用。此时,内存中有一些列表对象和对它的引用,您可以使用名称a 访问它。

>>> b = [0]

这对b 做同样的事情。有一个新列表存储在内存位置 1002。编程语言会创建一个引用 b,您可以使用它来引用内存位置,进而引用列表对象。

>>> a[0], b[0] = b, a

这做了两件相同的事情,所以让我们专注于一件事情:a[0] = b。这做的很花哨。它首先评估等式的右侧,查看变量b 并获取内存中的相应对象(内存对象#1002),因为b 是对它的引用。左侧发生的事情同样精彩。 a 是一个指向列表(内存对象#1001)的变量,但内存对象#1001 本身有许多自己的引用。与您使用的名称为 ab 的引用不同,这些引用具有像 0 这样的数字索引。所以,现在,它的作用是a 拉起内存对象#1001,它是一堆索引引用,它转到索引为0 的引用(之前,这个引用指向实际数字0,它是您在第 1 行中所做的事情),然后将该引用(即内存对象#1001 中的第一个也是唯一的引用)重新指向等式右侧的事物的计算结果。所以现在,对象 #1001 的第 0 个引用指向对象 #1002。

>>> a
[[[...]]]
>>> b
[[[...]]]

这只是编程语言的奇思妙想。当您只要求它评估 a 时,它会拉起内存对象(位置 #1001 的列表),使用它自己的魔法检测它是无限的并呈现自己。

>>> a == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp

此语句的失败与 Python 进行比较的方式有关。当您将一个对象与其自身进行比较时,它会立即评估为真。当您比较和反对另一个对象时,它使用“魔术”来确定相等是真还是假。对于 Python 中的列表,它会查看每个列表中的每个项目并检查它们是否相等(进而使用项目自己的相等检查方法)。所以,当你尝试a == b。它所做的是首先挖掘 b (object #1002) 和 a (object #1001),然后意识到它们在内存中是不同的东西,所以转到它的递归列表检查器。它通过遍历这两个列表来做到这一点。对象#1001 有一个索引为0 的元素指向对象#1002。对象#1002 有一个索引为0 的元素指向对象#1001。因此,程序得出结论,如果对象#1001 和#1002 的所有引用都指向同一事物,则它们是相等的,因此如果#1002(#1001 的唯一引用指向的对象)和#1001(#1002 的唯一引用指向的对象)是同一件事情。这种平等检查永远不会停止。任何不停止的列表都会发生同样的事情。你可以这样做 c = [0]; d = [0]; c[0] = d; d[0] = ca == c 会引发同样的错误。

>>> a[0] == b
True

正如我在上一段中所暗示的,这立即解析为 true,因为 Python 采用了捷径。它不需要比较列表内容,因为a[0] 指向对象#1002,b 指向对象#1002。 Python 检测到它们在字面意义上是相同的(它们是相同的“事物”),甚至不会检查内容。

>>> a[0][0] == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp

这又回到了错误,因为a[0][0] 最终指向对象#1001。身份检查失败并退回到递归内容检查,这永远不会结束。

>>> a[0][0][0] == b
True

a[0][0][0] 再次指向对象 #1002,b 也是如此。递归检查被跳过,比较立即返回真。


更高级别的jibber jabber与你的具体代码sn-p没有直接关系:

由于所有的引用都指向其他对象,即使存在看似“无限”的嵌套,a 引用的对象(我称之为对象#1001)和引用的对象be b (#1002) 在内存中的大小相同。而且这个大小实际上非常小,因为它们都是指向各自其他内存位置的列表。 equals())来进行自定义相等测试。 Python 对列表进行了开箱即用的处理。我不特别了解 Python,但至少在 Ruby 中,== 是重载的,因为当你执行someobject == otherobject 时,它实际上在someobject 上调用了一个名为== 的方法(你可以覆盖)。理论上,没有什么能阻止您让 someobject == otherobject 返回布尔值以外的值。

【讨论】:

+1 以获得很好的详细答案。我唯一可能抱怨的是[0] 在 Python 中被称为 list,而不是 array。还有arrays,但它们不像列表那样促进循环引用。 @SvenMarnach:感谢您指出这一点。我将进行快速编辑,以免将来的人们感到困惑。为什么数组不支持循环引用?他们会在重新分配时被克隆吗? @StevenXu:数组只能容纳非常有限的对象类型——请参阅上面的链接。特别是,它们不能保存任意 Python 对象或其他数组。【参考方案2】:

怀疑会发生以下情况:

a[0]==b:Python 查找值a[0] 并找到对b 的某种引用,因此它显示为True

a[0][0]==b:Python 查找a[0],找到b,现在查找a[0][0],即,(因为a[0] 持有bb[0]。现在它看到,b[0] 持有对a 的某种引用,这与b 并不完全相同。所以python必须比较元素,这意味着它必须比较a[0]b[0]。现在,无限递归开始……

请注意,这仅是因为 Python 在分配 a[0]=b 时实际上并未复制列表。 Python 会创建一个对 b 的引用,该引用存储在 a[0] 中。

【讨论】:

【参考方案3】:

a[0] 指的是bb[0] 指的是a。这是一个循环引用。正如glglgl 所提到的,当您使用== 运算符时,它会比较值。

试试这个,可能会让事情更清楚 -

>>> id(a)
4299818696
>>> id(b)
4299818768
>>> id(a[0])
4299818768
>>> 
>>> id(b[0])
4299818696

【讨论】:

这是一个很好的答案。它非常简单地解释了两个列表的存储方式。【参考方案4】:

我明白了,a 包含 b,它包含 a

它们不包含彼此 - A 是对列表的引用,该列表中的第一件事是对 B 的引用,反之亦然

>>> a[0] == b
True
>>> a[0][0] == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp
>>> a[0][0][0] == b
True

此处 [0] 的数量无关紧要,因为您可以进行任意数量的列表查找 - 重要的是在示例 #1 和 #3(以及所有奇数的查找)中,您正在说“B等于B”,此时python比较内存地址并看到它们是同一件事,所以说是。对于示例#2(甚至所有查找),您说“A等于B”,python看到它们是不同的内存地址,然后尝试将整个(无限)数据结构加载到内存中以进行更多操作-深度比较。

【讨论】:

【参考方案5】:

这是两个列表。首先,您创建它们:

a = [0]
b = [0]

然后,将每个元素分配给另一个元素的第一个元素:

a[0], b[0] = b, a

所以你可以说

a[0] is b

b[0] is a

这与第一个例子的情况相同,但更深一层。

此外,您不比较身份 (is),而是比较平等 (==)。这导致尝试比较它们 - 深入内心,导致递归。

【讨论】:

is 的好东西。我没想过这样比较。

以上是关于无限嵌套列表中到底发生了啥?的主要内容,如果未能解决你的问题,请参考以下文章

RecyclerView里面嵌套一个无限循环的横向列表该怎么做

Sencha Touch 1 的嵌套列表无限循环

RecyclerView里面嵌套一个无限循环的横向列表该怎么做

如何在颤动的列表视图中嵌套列表视图?

android“强制关闭”内存到底发生了啥

C++ 这里到底发生了啥? [关闭]