Оператор '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 работает под капотом:-)