Оператор 'is' ведет себя неожиданно с не кэшированными целыми числами

При игре с интерпретатором Python я наткнулся на этот конфликтный случай относительно оператора is:

Если оценка выполняется в функции, она возвращает True, если она выполняется вне нее, возвращает False.

>>> def func():
...     a = 1000
...     b = 1000
...     return a is b
...
>>> a = 1000
>>> b = 1000
>>> a is b, func()
(False, True)

Так как оператор is оценивает id() для задействованных объектов, это означает, что a и b указывают на тот же экземпляр int, объявленный внутри функции func, но, наоборот, они указывают на другой объект, находящийся за его пределами.

Почему это так?


Примечание. Я знаю разницу между идентичностью (is) и операциями равенства (==), как описано в Понимание Python "is" оператор. Кроме того, я также знаю о кэшировании, которое выполняется python для целых чисел в диапазоне [-5, 256], как описано в "is" оператор ведет себя неожиданно с целыми числами.

Этот здесь не относится, так как числа находятся за пределами этого диапазона, а Я хочу оценить идентичность и не.

Ответ 1

ТЛ; др:

В справочном руководстве говорится:

Блок представляет собой часть текста программы Python, которая выполняется как единое целое. Ниже приведены блоки: модуль, тело функции и определение класса. Каждая команда, введенная интерактивно, является блоком.

Вот почему в случае функции у вас есть кодовый блок single, который содержит одиночный объект для числового литерала 1000, поэтому id(a) == id(b) даст True.

Во втором случае у вас есть два разных объекта кода, каждый со своим собственным другим объектом для литерала 1000, поэтому id(a) != id(b).

Обратите внимание, что это поведение не проявляется только с литералами int, вы получите похожие результаты, например, с литералами float (см. здесь).

Конечно, сравнение объектов (за исключением явных тестов is None) всегда должно выполняться с помощью оператора равенства ==, а не is.

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


Более длинный ответ:

Чтобы получить немного более четкое представление и дополнительно проверить это, казалось бы, странное поведение, мы можем смотреть прямо в объектах code для каждого из этих с использованием модуля dis.

Для функции func:

Наряду со всеми другими атрибутами объектные объекты также имеют атрибут __code__, который позволяет заглянуть в скомпилированный байт-код для этой функции. Используя dis.code_info, мы можем получить красивое представление обо всех хранимых атрибутах в объекте кода для данной функции:

>>> print(dis.code_info(func))
Name:              func
Filename:          <stdin>
Argument count:    0
Kw-only arguments: 0
Number of locals:  2
Stack size:        2
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
   1: 1000
Variable names:
   0: a
   1: b

Нам интересна запись Constants для функции func. В нем мы видим, что мы имеем два значения: None (всегда присутствует) и 1000. У нас есть только одиночный int-экземпляр, который представляет константу 1000. Это значение, которое a и b будут назначаться при вызове функции.

Доступ к этому значению легко через func.__code__.co_consts[1], и поэтому другой способ просмотра нашей оценки a is b в функции будет таким:

>>> id(func.__code__.co_consts[1]) == id(func.__code__.co_consts[1]) 

Что, конечно, будет оцениваться как True, потому что мы имеем в виду один и тот же объект.

Для каждой интерактивной команды:

Как отмечалось ранее, каждая интерактивная команда интерпретируется как единый кодовый блок: анализируется, компилируется и оценивается независимо.

Мы можем получить объекты кода для каждой команды через compile встроенный:

>>> com1 = compile("a=1000", filename="", mode="single")
>>> com2 = compile("b=1000", filename="", mode="single")

Для каждого оператора присваивания мы получим аналогичный объект кода, который выглядит следующим образом:

>>> print(dis.code_info(com1))
Name:              <module>
Filename:          
Argument count:    0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             NOFREE
Constants:
   0: 1000
   1: None
Names:
   0: a

Такая же команда для com2 выглядит одинаково, но имеет принципиальное отличие: каждый из объектов кода com1 и com2 имеет разные int-экземпляры, представляющие литерал 1000. Вот почему в этом случае, когда мы делаем a is b через аргумент co_consts, мы фактически получаем:

>>> id(com1.co_consts[0]) == id(com2.co_consts[0])
False

Что согласуется с тем, что мы действительно получили.

Различные объекты кода, различное содержимое.


Примечание: Мне было любопытно, как именно это происходит в исходном коде, и после того, как он прорыт его, я верю, что нашел его.

В фазе компиляции объект co_consts представлен объектом словаря. В compile.c мы можем увидеть инициализацию:

/* snippet for brevity */

u->u_lineno = 0;
u->u_col_offset = 0;
u->u_lineno_set = 0;
u->u_consts = PyDict_New();  

/* snippet for brevity */

Во время компиляции это проверяется на наличие уже существующих констант. См. @Раймонд Хеттингер ниже для более подробной информации.


Предостережение:

  • Приведенные в цепочку выражения будут проверяться на проверку идентичности True

    Теперь должно быть более понятно, почему именно следующее оценивается как True:

    >>> a = 1000; b = 1000;
    >>> a is b
    

    В этом случае, объединив две команды присваивания, мы попросим интерпретатора скомпилировать эти вместе. Как и в случае для объекта функции, будет создан только один объект для литерала 1000, что приведет к значению True при оценке.

  • Выполнение на уровне модуля возвращает True снова:

    Как упоминалось ранее, в справочном руководстве указано, что:

    ... Ниже приведены блоки: модуль...

    Итак, применяется одно и то же предположение: у нас будет один объект кода (для модуля), и поэтому в результате отдельные значения сохраняются для каждого другого литерала.

  • Тот же не применяется для объектов изменчивых:

    Это означает, что если мы явно не инициализируем один и тот же изменяемый объект (например, с a = b = []), то идентификатор объектов никогда не будет равен, например:

    a = []; b = []
    a is b  # always returns false
    

    Опять же, в документации, это указано:

    после a = 1; b = 1, a и b могут или не могут ссылаться на один и тот же объект со значением один, в зависимости от реализации, но после c = []; d = [], c и d гарантированно относятся к двум различным уникальным, вновь созданным пустым спискам.

Ответ 2

В интерактивном приглашении запись скомпилирована в одном режиме, которая обрабатывает один полный оператор за раз. Сам компилятор (в Python/compile.c) отслеживает константы в словаре под названием u_consts, который сопоставляет постоянный объект с его индексом.

В функции compiler_add_o() вы увидите, что перед добавлением новой константы (и приращением индекса), dict проверяется, чтобы видеть существует ли постоянный объект и индекс. Если это так, они используются повторно.

Короче говоря, это означает, что повторяющиеся константы в одном утверждении (например, в определении вашей функции) складываются в один синглтон. Напротив, ваши a = 1000 и b = 1000 - это два отдельных утверждения, поэтому сгибание не происходит.

FWIW, все это всего лишь деталь реализации CPython (т.е. не гарантируется языком). Вот почему приведенные здесь ссылки относятся к исходному коду C, а не к языковой спецификации, которая не дает никаких гарантий по этому вопросу.

Надеюсь, вам понравилось это понимание того, как CPython работает под капотом: -)