无限嵌套列表中到底发生了啥?
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
指向的事物的引用。
变量a
和b
本身甚至不是“事物”本身,它们本身也只是指向内存中其他匿名“事物”的指针。
引用的概念是非编程领域的一次重大飞跃,因此让我们牢记这一点来逐步完成您的程序:
>>> 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 本身有许多自己的引用。与您使用的名称为 a
和 b
的引用不同,这些引用具有像 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] = c
和 a == 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]
持有b
)b[0]
。现在它看到,b[0]
持有对a
的某种引用,这与b
并不完全相同。所以python必须比较元素,这意味着它必须比较a[0]
和b[0]
。现在,无限递归开始……
请注意,这仅是因为 Python 在分配 a[0]=b
时实际上并未复制列表。 Python 会创建一个对 b
的引用,该引用存储在 a[0]
中。
【讨论】:
【参考方案3】:a[0]
指的是b
,b[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里面嵌套一个无限循环的横向列表该怎么做