Оператор '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", оператор ведет себя неожиданно с целыми числами.

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

2 ответа

Решение

ТЛ; др:

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

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

Вот почему в случае функции у вас есть один блок кода, который содержит один объект для числового литерала 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 */

Во время компиляции это проверяется на наличие уже существующих констант. Смотрите ответ @Raymond Hettinger ниже, чтобы узнать больше об этом.


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

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

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

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

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

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

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

    ... Следующие блоки: модуль...

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

  • То же самое не относится к изменяемым объектам:

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам