2011-10-06 55 views
29

可以在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 
>>> 

每个更深刻,当我试图去了解它,我觉得更像是我的大脑会爆炸。我看到,一个包含b,包含一个等...

现在我对这个问题。我们真的有两个列表吗?或者只有一个?这样的事情是如何存储在内存中的?有什么可以让程序员执行这样奇怪的事情?

请不要把这个问题看得超严重。别忘了,有时编程可能很有趣。

+1

这真是一个有趣的功能。 – phimuemue

+0

真棒问题。我非常喜欢Python的这个功能,尽管我从来没有找到它的用处。如果有人能够提出这个特性的实际应用,那将是非常好的。或者写一个模块来生成包含所有列表的列表:P – andronikus

+1

@andronikus:http://xkcd.com/468/ –

回答

24

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

伟大的问题。我认为中央误解(如果我甚至不能称呼它,它是完全合理的,你如何到达你所使用的思维过程)您遇到提示你要问的问题是这样的:

当我写b[0] = a时,并不意味着ab。这意味着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的比较方式有关。当你将一个对象与自己进行比较时,它立即计算结果为true。当你比较和对象到另一个对象时,它使用“魔术”来确定等式应该是真还是假。对于Python中的列表,它会查看每个列表中的每个项目,并检查它们是否相等(依次使用项目自己的等式检查方法)。所以,当你尝试a == b。它所做的是首先挖掘b(对象#1002)和一个(对象#1001),然后意识到它们在内存中是不同的东西,所以它的递归列表检查器。它通过迭代两个列表来完成。对象#1001有一个索引为0的元素指向对象#1002。对象#1002有一个索引为0的元素指向对象#1001。因此,如果程序#1001和#1002(#1001的唯一参考点)和#1001(#1002的唯一参考点)是否相同,则程序得出结论:如果对象#1001和#1002的所有引用都指向相同的东西,一样的东西。这种平等检查永远不会停止。同样的事情会发生在任何不停止的列表中。你可以做c = [0]; d = [0]; c[0] = d; d[0] = ca == c会引发同样的错误。

>>> a[0] == b 
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。递归检查被跳过,比较立即返回true。


更高级别的胡言乱语胡言乱语不直接关系到你的特定代码片段:

  • 由于所有有所指的其他对象的引用,即使有这似乎是“无限”的嵌套,由a(正如我所说的对象#1001)引用的对象和被称为b(#1002)的对象在内存中都是相同的大小。而且这个尺寸实际上非常小,因为它们都是指向其他存储位置的列表。
  • 另外值得一注意,在更短的“大手笔”的语言,比较==回报true只有如果它们指向的内存对象是在这个意义上,这两个引用指向内存中的相同点相同的两个引用。 Java就是这样的一个例子。已经出现在这种语言中的文体惯例是定义对象本身的方法/函数(对于Java,通常称为equals())来执行自定义相等性测试。 Python为列表开箱即用。我不太了解Python,但至少在Ruby中,==负载过重,因为在执行someobject == otherobject时,它实际上调用someobject(可以覆盖)的==方法。从理论上讲,让someobject == otherobject返回布尔值以外的内容没有任何阻碍。
+0

很好的回答。真的,我无能为力,只是接受它而已。 – Gandi

+0

+1一个不错的和详细的答案。我唯一可能抱怨的是'[0]'在Python中被称为* list *,而不是*数组*。也有[数组](http://docs.python.org/library/array.html),但它们不像列表那样有助于循环引用。 –

+0

@SvenMarnach:谢谢你指出。我会快速编辑,以便将来的人不会感到困惑。为什么数组不支持循环引用?他们克隆重新分配还是什么? –

11

嫌疑会发生以下情况:

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]。现在,无限递归开始......

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

+0

这是一个很好的解释。我想,我开始明白这一点。 – Gandi

4

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

a = [0] 
b = [0] 

然后,可以为每个一个到另一个的第一个元素:

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

因此可以这样说

a[0] is b 

b[0] is a 

这是一样的情况离子作为第一个例子,但服从更深。

此外,你不比较的身份(is),但对于平等(==)。这导致尝试比较它们 - 深入内部,导致递归的原因。

+0

用'is'好东西。我没有想过用这种方式比较它。 – Gandi

7

我看到,一个包含B,包含

他们不互相牵制这样 - 一个是一个列表的引用,在此列表中的第一件事情是一个参考对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比较内存地址a看到他们是同一件事,所以说是的。通过例子#2(以及所有的偶数查找),你会说“是A等于B”,python认为它们是不同的内存地址,然后尝试将整个(无限)数据结构加载到内存中,深度比较。

10

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

试试这个,这可能会使事情更清晰 -

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

这是一个很好的答案。它解释非常简单,两个列表如何存储。 – Gandi