32

在 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 等等......

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

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

4

5 回答 5

31

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

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

当我写b[0] = a的时候,并不代表a就是 b。这意味着它b包括一个指向所指向事物的引用a

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

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

>>> a = [0]

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

>>> b = [0]

这对b. 有一个新列表存储在内存位置 1002 中。编程语言创建一个引用b,您可以使用它来引用内存位置,进而引用列表对象。

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

这做了两件相同的事情,所以让我们专注于一件事情:a[0] = b. 这做的很花哨。它首先评估等式的右侧,查看变量b并获取内存中的相应对象(内存对象#1002),因为b它是对它的引用。左侧发生的事情同样精彩。a是一个指向列表的变量(内存对象#1001),但内存对象#1001 本身有许多自己的引用。与您使用的名称类似aandb的引用不同,这些引用具有数字索引,例如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 与您的特定代码段没有直接关系:

  • 由于所有的都是引用其他对象的引用,即使存在看似“无限”的嵌套,所引用的对象a(如我所说的对象 #1001)和所引用的对象b(#1002)都是两者在内存中的大小相同。而且这个大小实际上非常小,因为它们都是指向各自其他内存位置的列表。
  • 还值得注意的是,在不太“慷慨”的语言中,仅当两个引用指向的内存对象相同时才==比较两个引用与返回,即两个引用都指向内存中的同一点。Java 就是一个例子。在此类语言中出现的风格约定是在对象本身上定义一个方法/函数(对于 Java,通常称为)来进行自定义相等测试。Python 对列表进行了开箱即用的处理。我不特别了解 Python,但至少在 Ruby 中,它在某种意义上是重载的,当你这样做时,它实际上调用了一个调用的方法(你可以覆盖)。理论上,没有什么能阻止你制作true equals()==someobject == otherobject==someobjectsomeobject == otherobject返回布尔值以外的东西。
于 2011-10-06T20:32:19.693 回答
12

怀疑会发生以下情况:

a[0]==b:Python 查找该值a[0]并找到某种对 的引用b,所以它说True

a[0][0]==b: Python 查找a[0],找到b,现在查找a[0][0],即 (since a[0]hold b) b[0]。现在它看到了,它b[0]包含对 的某种引用a,它与 不完全相同b。所以 python 必须比较元素,这意味着它必须a[0]b[0]. 现在,无限递归开始......

请注意,这仅是因为 Python 在分配a[0]=b. Python 而是创建一个对b存储在a[0].

于 2011-10-06T13:17:10.660 回答
10

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

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

>>> id(a)
4299818696
>>> id(b)
4299818768
>>> id(a[0])
4299818768
>>> 
>>> id(b[0])
4299818696
于 2011-10-06T13:24:24.480 回答
7

我看到,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看到它们是不同的内存地址,然后尝试将整个(无限)数据结构加载到内存中以进行更多操作深度比较。

于 2011-10-06T13:23:39.003 回答
4

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

a = [0]
b = [0]

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

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

所以你可以说

a[0] is b

b[0] is a

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

此外,您不比较身份 ( is),而是比较相等 ( ==)。这导致尝试比较它们——在内心深处,是什么导致了递归。

于 2011-10-06T13:18:40.130 回答