据我所知,使用
[]
、{}
或 ()
实例化对象分别返回 list
、dict
或 tuple
的新实例;具有新身份的新实例对象。
这对我来说非常清楚,直到我实际测试它并且我注意到
() 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)
。()
可以说,这不足以回答我的问题。
那么,为什么空元组具有相同的标识,而列表或字典等其他元组却没有?
Python 在内部创建一个
C
元组对象列表,其第一个元素包含空元组。每次使用 tuple()
或 ()
时,Python 将返回上述 C
列表中包含的现有对象,而不是创建新对象。
对于
dict
或 list
对象来说,不存在这种机制,相反,每次都从头开始重新创建。
这很可能与不可变对象(如元组)无法更改这一事实有关,因此保证在执行期间不会更改。当考虑到
frozenset() is frozenset()
返回 True
时,这一点得到进一步巩固;就像 ()
一个空的 frozenset
在 CPython
的实现中被认为是单例。对于可变对象,这样的保证不存在,因此,没有动力缓存它们的零元素实例(即它们的内容可能会随着身份保持不变而改变)。
注意:这不是人们应该依赖的东西,即人们不应该将空元组视为单例。文档中没有明确做出此类保证,因此应该假设它是依赖于实现的。
在最常见的情况下,
CPython
的实现是用两个设置为正整数的宏PyTuple_MAXFREELIST
和PyTuple_MAXSAVESIZE
来编译的。这些宏的正值会导致创建大小为 tuple
的
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.. */
促使这样做的另一个原因是函数调用构造一个元组来保存将要使用的位置参数。这可以在load_args
中的
ceval.c
函数中看到:
static PyObject *
load_args(PyObject ***pp_stack, int na)
{
PyObject *args = PyTuple_New(na);
/* rest snipped for brevity.. */
do_call
调用。如果参数 na
的数量为零,则将返回一个空元组。
本质上,这可能是一个经常执行的操作,因此不每次都重建空元组是有意义的。
还有几个答案阐明了
CPython
的不可变缓存行为: