is vs ==, интернирование и мелкие числа: где интуиция C.pointer ломается
core правильно выбрать is или == в конкретной ситуации и объяснить, почему 'a is b' может внезапно быть True или False
В C сравнение указателей (p == q) и сравнение содержимого (*p == *q, strcmp, memcmp) — два явно разных выражения, и вы их не путаете, потому что синтаксис разный. В Python эта разница есть, но выражена выбором оператора:
a == b— равенство (equality): одинаковые ли значения. За это отвечает метод__eq__объекта. Аналогstrcmp/memcmp.a is b— идентичность (identity): являются лиaиbименами одного и того же объекта. Эквивалентid(a) == id(b). Прямой аналог сравнения указателей.
Правило простое и должно быть автоматическим: используйте ==, когда хотите узнать, равны ли значения; используйте is, только когда хотите узнать, является ли объект тем самым объектом — то есть практически всегда для сравнения с синглтоном None (а также True, False) и иногда для sentinel-объектов вашего собственного дизайна.
Осложняют дело оптимизации CPython: кэширование мелких целых чисел (обычно примерно от -5 до 256) и интернирование некоторых строк, похожих на идентификаторы. Одинаковые «мелкие» неизменяемые объекты не создаются заново — переиспользуется один объект. Из-за этого a = 256; b = 256; a is b часто оказывается True — не потому что язык это гарантирует, а потому что оба имени случайно ссылаются на один закэшированный объект. Стоит выйти за диапазон кэша (x = 257; y = 257) — is скорее всего станет False, хотя == по-прежнему True.
Это деталь реализации CPython, а не гарантия языка: диапазон может меняться между версиями и в других реализациях (PyPy, Jython). Код if val is 256: — бомба замедленного действия; правильный код — if val == 256:.
Разобранный пример
a = 256
b = 256
print(a is b) # обычно True — объект из кэша мелких int в CPython
c = 1000
d = 1000
print(c is d) # обычно False — за пределами кэша, два разных объекта
print(c == d) # True — сравниваются значения
e = 1000
f = e
print(e is f) # True — f привязан к тому же объекту, что и e
Рассуждение: первая пара совпадает по is не потому что «Python умный», а потому что реализация переиспользует предсозданные объекты для небольших чисел. Вторая пара — два отдельных литерала, каждый может создать свой объект; is здесь непредсказуем по спецификации, а == надёжен. Третья пара — не совпадение реализации, а прямой aliasing: здесь is гарантированно True.
То же с строками: литерал в коде может быть интернирован, а строка, собранная в рантайме, — нет:
status = "".join(["N", "E", "W"])
print(status == "NEW") # True — надёжно
print(status is "NEW") # False — другой объект
Поэтому проверка if task["status"] is "NEW": «работает» на литералах в тестах и молча ломается на данных из БД или API.
Отдельный частый случай — проверка на None:
if x is None: # правильно: None — единственный синглтон
...
if x == None: # работает, но зависит от __eq__ объекта x
...
is None предпочтителен не по стилю: он не зависит от того, как объект x определяет __eq__ — некоторые библиотечные объекты переопределяют == так, что x == None не возвращает обычное булево значение.
Попробуй сейчас
Не запуская код, ответьте и объясните почему:
[1, 2] is [1, 2]—TrueилиFalse?x = None; y = None; x is y—TrueилиFalse?s1 = "hello world!"; s2 = "hello world!"; s1 is s2— надёжноTrue, надёжноFalse, или зависит от реализации?
Ожидаемые ответы: (1) False — каждый литерал списка создаёт новый объект; изменяемые объекты не кэшируются. (2) True — None всегда один и тот же объект-синглтон. (3) Зависит от реализации — строки с пробелом обычно не интернируются автоматически (в отличие от строк, похожих на идентификаторы), гарантий нет; для сравнения значений всегда ==.
Получилось, если…
Вы разобрались, если на любое выражение с is можете сразу сказать: «это про идентичность конкретного объекта (синглтон/aliasing) или это случайность кэширования CPython, на которую нельзя опираться» — и для сравнения значений автоматически берёте ==, оставляя is для None, True, False и явных sentinel-объектов.
Вывод
is — идентичность (id), == — значение через __eq__; в C для этого были разные выражения, в Python — разные операторы, и их легко перепутать по инерции. Кэширование мелких int и интернирование части строк — оптимизации реализации, а не гарантии языка: код, который «работает» благодаря is для чисел или строк, — скрытый баг.
AI-generated · source-grounded review
🛡 Fact-checked: 2 risky claims verified · 4 removed · confidence: high · figures: 2 · a second model checked these claims and agreed
On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course