Почему '() is()' возвращает True, когда '[] is []' и '{} есть {}' return False?

Из того, что мне было известно, используя [], {} или () для создания экземпляров объектов, возвращает новый экземпляр 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). Если аргумент не задан, конструктор создает новый пустой кортеж, ().

Достаточно сказать, этого недостаточно для ответа на мой вопрос.

Итак, почему пустые кортежи имеют одинаковый идентификатор, в то время как другие, подобные спискам или словарям, не работают?

Ответ 1

Короче:

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, он должен добавить новый пустой кортеж в список, если он не работает 't уже существует:

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 с неизменяемыми:

  • Для целых чисел другой ответ, который выкапывает в источнике, можно найти здесь.
  • Для строк можно найти несколько ответов здесь, здесь и .