为什么当'() is ()'返回True时,'[] is []'和'{} is {}'返回False?

50
从我所了解的情况来看,使用[]{}()来实例化对象会返回一个新的listdicttuple实例;一个具有新身份的新实例对象。
在我实际测试之前,这对我来说是相当清楚的,但我注意到() is ()实际上返回的是True,而不是预期的False
>>> () is (), [] is [], {} is {}
(True, False, False)

如预期的那样,当使用list()dict()tuple()创建对象时,也会表现出相同的行为。
>>> tuple() is tuple(), list() is list(), dict() is dict()
(True, False, False)

我在 tuple() 的文档中找到的唯一相关信息如下:

[...] 例如,tuple('abc') 返回 ('a', 'b', 'c')tuple([1, 2, 3]) 返回 (1, 2, 3)如果没有提供参数,构造函数会创建一个新的空元组,()

可以说,这对于回答我的问题是不够的。
那么,为什么空元组具有相同的标识,而其他类型如列表或字典则不具备这种特性呢?

3
相关链接:CPython中元组是如何实现的? - Martijn Pieters
值得注意的是,这不仅仅是空元组的问题。例如,(1,) is (1,) 也可以返回 True - undefined
1个回答

55
简而言之:
CPython在内部创建了一个C列表,其中包含一个空元组作为第一个元素。每次使用tuple()()时,Python都会返回上述C列表中包含的现有对象,而不是创建一个新对象。
对于dictlist对象,不存在这样的机制,相反,它们在每次使用时都会从头开始重新创建
这很可能与不可变对象(如元组)无法更改的事实有关,并且因此在执行过程中保证不会发生更改。这一点在考虑到frozenset() is frozenset()返回True时更加明确;就像()一个空的frozenset 在CPython的实现中被视为单例一样。对于可变对象,这样的保证并不存在,因此没有动机来缓存它们的零元素实例(即它们的内容可能会改变,但标识保持不变)。
注意:这不是一个人应该依赖的东西,也就是说,不能把空元组视为单例。文档中没有明确提供这样的保证,所以应该假设它取决于具体的实现。

如何完成:

在最常见的情况下,CPython的实现是使用两个宏PyTuple_MAXFREELISTPyTuple_MAXSAVESIZE编译的,这两个宏的值设为正整数。这些宏的正值会导致创建一个大小为PyTuple_MAXSAVESIZE元组对象数组

当调用PyTuple_New并且参数size == 0时,它会确保如果列表中不存在空元组,则添加一个新的空元组

if (size == 0) {
    free_list[0] = op;
    ++numfree[0];
    Py_INCREF(op);          /* extra INCREF so that this is never freed */
}

然后,如果请求一个新的空元组,那么将返回位于此列表的第一个位置的元组,而不是一个新的实例。
if (size == 0 && free_list[0]) {
    op = free_list[0];
    Py_INCREF(op);
    /* rest snipped for brevity.. */

另一个导致这样做的原因是函数调用会构建一个元组来保存将要使用的位置参数。这可以在ceval.c中的load_args函数中看到。
static PyObject *
load_args(PyObject ***pp_stack, int na)
{
    PyObject *args = PyTuple_New(na);
    /* rest snipped for brevity.. */

在同一文件中通过do_call调用的函数被称为。如果参数na的数量为零,则将返回一个空元组。
本质上,这可能是一个经常执行的操作,因此不需要每次都重新构建一个空元组是有道理的。

进一步阅读:

还有几个答案揭示了CPython对不可变对象的缓存行为:

  • 关于整数,可以在这里找到另一个深入源码的答案。
  • 关于字符串,可以在这里这里这里找到一些答案。

4
@vijrox,肯定不被认为是一个 bug;我的建议是不要依赖这个,因为在Python 参考手册 中没有提到它们是单例。值得注意的单例,如 NoneEllipsis 等都有明确的说明,而 ()frozenset() 没有,因此可以安全地假设它们被视为实现细节,您不应该依赖它们。 - Dimitris Fasarakis Hilliard
2
没错 - 不过我指的是你在问题中指出的文档部分,它说:“如果没有给出参数,则构造函数创建一个新的空元组,()。” 这种行为似乎(对我来说)恰好与文档的那部分相矛盾。 - vijrox
3
@vijrox 啊,是的,这也让我感到困惑。也许这是为了不让参考手册过于混乱;也许是为了不强制其他实现有那种限制而写成那样。我无法确定,但我理解你的困惑。 - Dimitris Fasarakis Hilliard
9
@vijrox 不是的,这只是实现细节。空元组的行为完全符合语言规范。语言规范并没有说空元组不能是单例。同样,语言规范也没有规定小整数必须被缓存(然而,在CPython中它们确实被缓存了)。() is ()是否为True不能保证,就像3 is 3是否为True一样,并不会破坏元组类和用户之间的任何契约。 - Łukasz Rogalski
4
举例来说,pypy的实现方式不同。() is () 仍会返回True,但 () is tuple() 或者 tuple() is tuple() 则会返回False。而在jython中,这三种形式都会返回False。 - mata
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接