Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·30 МИН

is против ==: одно спрашивает про тождество, второе вызывает метод — и половина известных сюрпризов вообще не про них

«is сравнивает объекты, == сравнивает значения» — верно и бесполезно: на вопрос «почему у меня 1000 is 1000 даёт True» это не отвечает. Ответов там три разных: кеш малых чисел, объединение констант компилятором и интернирование строк. Опираться в коде нельзя ни на один из трёх.

Полное техническое изложение

TL;DR

Два оператора отвечают на два разных вопроса. is спрашивает: это тот же самый объект? == спрашивает: считаются ли эти значения равными? Первое — контракт языка: true if and only if x and y are the same object (истинно тогда и только тогда, когда x и y — один и тот же объект), и переопределить это нечем — метода __is__ нет. Второе — вызов __eq__, то есть обычного метода, который может вернуть что угодно и стоить сколько угодно. Поэтому is уместен там, где контракт самого API — про тождество: у объектов, существующих в единственном экземпляре, и у собственных меток, заведённых ровно ради сравнения по тождеству.

Отсюда главное следствие: is бывает истинным там, где вы этого не писали, — и не по одной причине, а по трём разным. Кеш малых чисел — от −5 до 256 включительно. Объединение равных констант компилятором — из-за него 1000 is 1000 в одном файле даёт True, а в разных файлах False. Интернирование строк — литерал, похожий на идентификатор, оказывается одним объектом на всю программу; на строку, собранную во время выполнения, полагаться нельзя. Различать эти три важнее, чем знать про каждый: ни один из них не обещан языком.

Дальше — то, что отличает знающего от читавшего. Границы кеша одинаковы в проверенных сборках 3.11–3.14, и компилятор с 3.8 предупреждает об is с литералом сам. Сравнением указателей тождество реализовано в CPython, и в другой реализации оно может быть устроено иначе. Цена: is — 11,8–12,1 нс независимо ни от чего; == на равных строках по двести тысяч символов — 4701 нс, в четыреста раз дороже (3.13.7). Но на коротких строках разница всего вдвое, и совет «пишите is» отсюда не следует.

Порог входа
Перед уроком достаточно понимать
  • имя в программе связано с объектом, и одному объекту может соответствовать несколько имён;
  • два разных объекта могут хранить одно и то же содержимое;
  • класс может задавать своё поведение операторов методами с двойным подчёркиванием.
Заранее знать не нужно
  • кеш малых чисел, объединение констант компилятором, интернирование строк;
  • __eq__, id(), sys.intern, nan, вечные объекты.

База: один и тот же объект — или одинаковое содержимое

Прежде чем говорить о кешах и интернировании, стоит назвать обычными словами, что спрашивает каждый из двух операторов. Вопроса ровно два, и они разные:

  • a is b спрашивает — это тот же самый объект?
  • a == b спрашивает — считаются ли эти значения равными?

Разницу видно на трёх переменных, где два имени указывают на один объект, а третье — на отдельный объект с тем же содержимым:

PYTHON
first = [1, 2, 3]
second = first              # второе имя для того же самого объекта
third = [1, 2, 3]           # отдельный объект, содержимое такое же
 
print(first is second)      # True  — объект один
print(first is third)       # False — объекта два
print(first == third)       # True  — содержимое одинаковое

Третья строка и есть вся суть: first и third равны и при этом не тождественны. Проверить это можно и с другой стороны — дописать элемент в first: second изменится вместе с ним, потому что это один объект, а third останется прежним.

И вот главный вопрос урока: почему тогда два одинаковых литерала иногда оказываются одним объектом? Написали 1000 дважды — а is отвечает True, хотя по смыслу это должно быть два разных объекта, как first и third.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования: is — про тождество, == — про равенство значений, и подменять один другим нельзя. Всё дальнейшее — про то, откуда берётся это лишнее True, почему причин у него три разных и почему ни на одну из них нельзя опираться в коде.

Механизм 1: что делает каждый из двух

контракт языкаГарантия языка: is истинно тогда и только тогда, когда это один и тот же объект. Сравнение указателей — уже реализация.

Определение is в справочнике занимает одно предложение, и в нём нет ни слова про значения.

The operators is and is not test for an object's identity: x is y is true if and only if x and y are the same object. An Object's identity is determined using the id() function.

перевод

Операторы is и is not проверяют тождество объекта: x is y истинно тогда и только тогда, когда x и y — один и тот же объект. Тождество объекта определяется функцией id().

Справочник языка, сравнение по тождеству

А что такое тождество — сказано в модели данных; там оно названо идентичностью, слово то же самое: An object's identity never changes once it has been created; you may think of it as the object's address in memory (Идентичность объекта никогда не меняется после его создания; можно считать её адресом объекта в памяти). Для CPython это не «можно считать», а буквально так и есть — там же стоит пометка о реализации.

Границу между этими двумя утверждениями стоит держать в голове весь урок. Контракт языка — «тот же объект или нет»; то, что тождество реализовано как адрес в памяти, — свойство CPython, а не обещание языка. Ответ «is сравнивает адреса» верен только как ответ на вопрос «как это сделано»; на вопрос «что это значит» он не отвечает.

Из этого следуют три вещи сразу:

  • is нельзя переопределить. Нет метода __is__. В CPython оператор компилируется в инструкцию IS_OP, которая сравнивает два указателя, — но это деталь реализации, а не определение оператора.
  • is не зависит от типа и размера. Сравнить два стомегабайтных объекта ровно так же дёшево, как два числа.
  • == — противоположность в каждом: его переопределяют, и он зависит и от типа, и от размера. Это вызов __eq__, который может вернуть не-булево значение, обойти миллион элементов или уйти в сеть.

Механизм 2: где is — правильный инструмент

Правило «пишите is только с None» короткое, но неверное: оно не про None, а про тождество как контракт. Есть объекты, у которых API обещает единственность, и спрашивать про них нужно именно is — сравнение по значению здесь либо лишнее, либо опасное. Прогон bench/identity/contract.py, блок 3:

None is None                                   True
NotImplemented is NotImplemented               True
Ellipsis is ...                                True
Color.RED is Color(1)                          True

Тот же список продолжается собственным часовым — и это тот случай, где is не заменить ничем:

PYTHON
MISSING = object()
 
def find(mapping, key):
    got = mapping.get(key, MISSING)
    return "ключа нет" if got is MISSING else got
 
find({"k": None}, "k")      # None — ключ есть, значение None
find({}, "k")               # 'ключа нет'

== MISSING тут не годится по той же причине, по которой не годится == None: равенство спрашивает у чужого объекта, а часовой — про тождество.

Важно, где проходит граница правила. Она не по списку из четырёх строк выше: None, NotImplemented, Ellipsis и члены Enum — это примеры, а не полный перечень. Правило общее: is уместен у любого объекта, который существует в единственном экземпляре, и у любой метки, заведённой специально ради сравнения по тождеству. Собственный MISSING = object() в этот список входит ровно на тех же правах, что и None, — и точно так же туда войдёт любой другой часовой, который вы заведёте завтра.

А вот is True и is False в этот список НЕ входят, хотя тоже одиночки: проверка x is True отклонит 1, непустую строку и всё прочее истинное. Это не вопрос о тождестве, это вопрос об истинности, заданный не тем оператором.

Механизм 3: где is даёт True не потому, что вы так написали

деталь реализации · CPython 3.13Кеш малых чисел, объединение констант и интернирование — устройство сборки. Опираться на них в коде нельзя.

Вот тут и живёт настоящая путаница. Ответ «is сравнивает объекты» не объясняет, почему 1000 is 1000 даёт True — а объясняют это три РАЗНЫХ механизма, и они разной прочности.

Под каждой строкой картинки стоит пометка, на чём держится ответ: на обещании справочника, на устройстве CPython или на том, попали ли литералы в одну компиляцию. В код переносится только первое.

Разберём каждый.

Кеш малых чисел. Числа от −5 до 256 создаются при старте интерпретатора и раздаются готовыми. Проверять это нужно осторожно — числами, которые компилятор не может посчитать заранее:

PYTHON
print(int("256") is int("256"))     # True
print(int("257") is int("257"))     # False

Границы одинаковы на 3.11, 3.12, 3.13 и 3.14.7. Но это деталь CPython, а не обещание языка.

Объединение констант. А вот это уже не про числа вовсе:

PYTHON
def first():  return 1000
def second(): return 1000
 
print(first() is second())          # True

Компилятор объединяет равные константы в пределах одной компиляции — даже когда они лежат в разных функциях. Перенесите одну функцию в другой файл, и тот же код даст False. То есть первое True — свойство того, как СОБРАН исходник, а не свойство чисел.

Интернирование строк. Правило простое: литерал, похожий на идентификатор, интернируется — один объект на всю программу, включая другие файлы. На автоматическое интернирование строки, собранной во время выполнения, полагаться нельзя — даже если по виду она идентификатор. Обратное тоже верно: is иногда даёт True и без интернирования, когда операция вернула тот же объект. Проверено: "".join([s]) с одним элементом, полный срез s[:] и str(s) возвращают исходную строку, а пустая строка и односимвольные latin-1 — вообще кешированные синглтоны.

PYTHON
import sys
 
ident = "hello_world"
built = "".join(["hello", "_", "world"])
 
print(built is ident)               # False
print(sys.intern(built) is ident)   # True

Документация говорит и зачем это сделано: the key comparisons (after hashing) can be done by a pointer compare instead of a string compare (сравнение ключей (после хеширования) может выполняться сравнением указателей вместо сравнения строк). Интернирование существует ради скорости словарей, а не ради того, чтобы вы писали is со строками.

Компилятор, кстати, предупреждает об этом сам, и с 3.8:

SyntaxWarning: "is" with 'str' literal. Did you mean "=="?

Механизм 4: nan — единственное значение, где расходится всё

PYTHON
nan = float("nan")
 
print(nan == nan)          # False
print(nan is nan)          # True
print(nan in [nan])        # True
print([nan] == [nan])      # True

Первая строка — требование стандарта с плавающей точкой. Третью объясняет справочник — он даёт точную эквивалентность для проверки вхождения.

For container types such as list, tuple, set, frozenset, dict, or collections.deque, the expression x in y is equivalent to any(x is e or x == e for e in y).

перевод

Для контейнерных типов, таких как list, tuple, set, frozenset, dict или collections.deque, выражение x in y эквивалентно any(x is e or x == e for e in y).

Справочник языка, проверка вхождения

Тождество проверяется первым — это записано прямо в порядке операндов or. Поэтому тот же самый nan в списке находится, а другой nan с тем же значением — нет:

PYTHON
print(float("nan") in [nan])    # False

Четвёртая строка держится на том же порядке: список сравнивает элементы сначала по тождеству и только потом по равенству. Но это уже устройство CPython, а не обещание языка — справочник описывает так проверку вхождения, а не сравнение списков.

То же правило объясняет, почему nan работает ключом словаря: положили и достали одним и тем же объектом — сработало тождество.

Механизм 5: is None, а не == None

Правило известное, а причина у него ровно одна: == — это вызов метода, и метод может соврать.

PYTHON
class AlwaysEqual:
    def __eq__(self, other):
        return True
 
obj = AlwaysEqual()
print(obj == None)      # True
print(obj is None)      # False

Причём __eq__ не обязан возвращать даже булево значение:

PYTHON
class Weird:
    def __eq__(self, other):
        return "не булево значение"
 
print(bool(Weird() == 1))       # True — непустая строка истинна

if x == None: в такой ситуации молча уйдёт не в ту ветку. is None перехватить нечем: он спрашивает про тождество, а метода для этого вопроса нет.

То же касается is True и is False, но с обратным выводом: писать так не надо. Проверка x is True отклонит 1, непустую строку и всё остальное истинное — почти всегда это ошибка, а не намерение.

Механизм 6: id уникален не всегда

PYTHON
print(id([]) == id([]))     # True

Никакого чуда: первый список умирает до того, как создаётся второй, и адрес достаётся второму. Документация оговаривает это прямо.

This is an integer which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id() value.

перевод

Это целое число, гарантированно уникальное и постоянное для данного объекта в течение его жизни. Два объекта с непересекающимися временами жизни могут иметь одно и то же значение id().

Встроенные функции, id()

Практический вывод: id(a) == id(b) — не замена a is b, а его худшая версия. Она даёт тот же ответ, только когда оба объекта живы; сохранённый в переменную id не значит ничего.

Глубже: что это стоит

наблюдение замераbench/identity/cost.py, CPython 3.13.7. Абсолютные наносекунды зависят от машины; содержателен рост == вместе с длиной.

Картинка говорит не «пишите is» — она показывает, как устроена цена каждого из двух.

is — постоянная цена: в CPython сравниваются два указателя, и делается это одинаково, идёт ли речь о числе или о строке в двести тысяч символов. == растёт вместе с длиной, потому что равные строки приходится дочитать до конца: различия, на котором можно остановиться, у них нет. Строки, различающиеся с первого символа, сравниваются быстрее равных — 19,58 против 24,62 нс. В таблице роста то же сравнение дало 24,29: числа сравнимы только внутри своего блока, а разница в четверть процента между блоками — шум машины.

Отдельно стоит объект со своим __eq__ на Python: 101,83 нс против 24,68 нс у объекта без него, то есть вчетверо. Метод на Python дороже сравнения, которое интерпретатор делает сам, и это стоит помнить в горячем цикле — но чинится это не заменой на is, а тем, чтобы не сравнивать в цикле лишнего.

Как отвечать на собеседовании

Короткий ответ: is спрашивает, один ли это объект; его нельзя переопределить, и его цена не зависит от объекта. == вызывает __eq__, обычный метод, который может вернуть что угодно. is уместен там, где контракт API именно про тождество: у любого объекта-одиночки и у любой метки, заведённой ради такого сравнения, — None, NotImplemented, Ellipsis, члены Enum, собственный часовой object().

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Первое, что стоит развести самому, не дожидаясь вопроса: сравнение по тождеству — контракт языка, а адрес в памяти — деталь CPython. Ответ «is сравнивает адреса» отвечает на вопрос «как это сделано», а не на вопрос «что оператор обещает»; на другой реализации тождество может быть устроено иначе, и контракт от этого не изменится.

Если спросят про 1000 is 1000, правильный ответ — назвать все три механизма и не перепутать их: кеш малых чисел кончается на 256, объединение констант работает в пределах одной компиляции, интернирование — про строки, похожие на идентификаторы. И добавить, что компилятор с 3.8 предупреждает об этом сам, формулируя ровно так: These can often work by accident in CPython, but are not guaranteed by the language spec (В CPython они часто работают случайно, но спецификацией языка не гарантированы).

И одна вещь, на которой легко переусердствовать, — цена. Сказать «is в четыреста раз дешевле» значит выдать один замер за общее правило: четыреста получились на равных строках по двести тысяч символов и на одной машине, а на коротких строках разница всего вдвое. Правило пишется не от числа, а от границы: is стоит одинаково всегда, == растёт вместе с длиной, и выбирают между ними не по цене, а по тому, какой вопрос задан.

Дальше спросят

Спросят дальше

Сравнили два одинаковых литерала через is, получили True. Можно на это опираться?

Короткий ответ

Нет. True здесь получается не потому, что так написано в коде, а потому, что интерпретатор переиспользовал объект — это деталь реализации, а не гарантия языка, и на другой сборке или в другом контексте её может не быть.

Спросят дальше

Есть значение, для которого x == x ложно. Какое и что из этого следует?

Короткий ответ

nan — единственное, где расходится всё: x == x даёт False, а x is x по-прежнему True. Отсюда и практическое следствие про контейнеры: поиск в них устроен так, что nan в списке находится, хотя равным самому себе не является.

Спросят дальше

id() уникален?

Короткий ответ

В пределах времени жизни объекта — да. Но после освобождения объекта тот же id может достаться другому, поэтому хранить id и сравнивать его позже — способ получить ложное совпадение.

Частые заблуждения

Утверждение

1000 is 1000 даёт True из-за кеша чисел

На самом деле

Кеш кончается на 256 — int("257") is int("257") даёт False. True в исходнике получается по другой причине: компилятор объединяет равные константы в пределах ОДНОЙ компиляции, и работает это даже для литералов в разных функциях одного файла. Тот же литерал из другого файла даёт False. Проверено на 3.11, 3.12, 3.13 и 3.14.7.

Утверждение

Все одинаковые строковые литералы — один объект

На самом деле

Только похожие на идентификатор. "hello_world" интернируется и совпадает даже между файлами, "hello world" с пробелом — нет. А на автоматическое интернирование строки, собранной во время выполнения, полагаться нельзя: "".join(["hello", "_", "world"]) даёт новый объект, пока его не пропустят через sys.intern. Но и обратного правила нет — "".join([s]) с одним элементом вернёт саму s.

Утверждение

nan не равен себе, значит его нельзя найти в списке

На самом деле

Находится. Справочник даёт точную эквивалентность: x in y — это any(x is e or x == e for e in y), и тождество проверяется ПЕРВЫМ. Тот же самый объект nan находится по is, а другой nan с тем же значением — нет. По той же причине nan работает ключом словаря, если класть и доставать одним объектом.

Утверждение

id(a) == id(b) — то же самое, что a is b

На самом деле

Только пока оба объекта живы. Документация оговаривает прямо: два объекта с непересекающимися временами жизни могут иметь одинаковый id. Отсюда id([]) == id([]), дающее True: первый список умирает до создания второго, и адрес достаётся второму.

Утверждение

is быстрее, поэтому там, где можно, лучше писать его

На самом деле

«Там, где можно» — это не список, а правило: любой объект, существующий в единственном экземпляре, и любая метка, заведённая ради сравнения по тождеству. None, NotImplemented, Ellipsis, члены Enum и собственный object() — примеры этого правила. На равных строках по 200 символов разница между is и == всего вдвое (12,01 против 24,29 нс, 3.13.7); четырёхсоткратной она становится на строках по двести тысяч символов, а такие в сравнении встречаются редко. Главное же в другом: на РАЗНЫХ объектах с одинаковым значением is отвечает неверно, и делает это быстро.

История версий

ВерсияИзменениеЧто это значит для кода
3.8Компилятор начинает выдавать SyntaxWarning на is с литералом — с прямой формулировкой причины: в CPython такие проверки часто работают случайно, но спецификацией не гарантированы. Практически это значит, что половину ошибок этого урока теперь ловит сам компилятор, если не глушить предупреждения.
3.12PEP 683: None, True, False и малые числа становятся вечными и перестают считать ссылки вовсе. На is это не влияет никак, но меняет вывод sys.getrefcount: вместо осмысленного числа он возвращает служебную константу. Читать её как «сколько ссылок» больше нельзя.
3.14Значение той самой служебной константы меняется: на 3.12 и 3.13 sys.getrefcount(None) даёт 4294967295, на 3.14.7 — 3221225472. Ни на одну проверку тождества это не влияет. Читать это число как счётчик ссылок нельзя ни в одной версии: оно служебное, и значение у него своё в каждой.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Числа получены вычислением, а не литералом, поэтому объединение констант в дело не вмешивается. Что напечатает этот код?
a = int("257")
b = int("257")
x = int("255")
y = int("255")

print(a == b)
print(a is b)
print(x is y)

Практика · оцените

Две равные строки по тридцать тысяч знаков: сравнение через == и через is. Во сколько раз дороже ==?
раза

Проверьте себя

Вопрос 1 из 5

В одном файле: def first(): return 1000 и def second(): return 1000. Что вернёт first() is second() и почему?

Чем измерено

Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Источники и что читать дальше

7 ИСТОЧНИКОВ

  1. Справочник языка — сравнение по тождествуОфициальная документация. Определение оператора дословно: «The operators is and is not test for an object's identity: x is y is true if and only if x and y are the same object. An Object's identity is determined using the id() function» (Операторы is и is not проверяют тождество объекта: x is y истинно тогда и только тогда, когда x и y — один и тот же объект. Тождество объекта определяется функцией id()). Из этого определения следует всё остальное: перехватить оператор нечем, и от типов он не зависит.https://docs.python.org/3.14/reference/expressions.html#is-not
  2. Модель данных — идентичность объектаОфициальная документация. Откуда взято утверждение, что идентичность не меняется: «Every object has an identity, a type and a value. An object's identity never changes once it has been created; you may think of it as the object's address in memory» (У каждого объекта есть идентичность, тип и значение. Идентичность объекта никогда не меняется после его создания; можно считать её адресом объекта в памяти). Там же отдельной пометкой о реализации: «For CPython, id(x) is the memory address where x is stored» (В CPython id(x) — это адрес в памяти, по которому хранится x).https://docs.python.org/3.14/reference/datamodel.html#objects-values-and-types
  3. Встроенные функции — id()Официальная документация. Ключевая оговорка урока про сравнение id, дословно: «This is an integer which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id() value» (Это целое число, гарантированно уникальное и постоянное для данного объекта в течение его жизни. Два объекта с непересекающимися временами жизни могут иметь одно и то же значение id()). Отсюда и id([]) == id([]), дающее True.https://docs.python.org/3.14/library/functions.html#id
  4. Справочник языка — проверка вхожденияОфициальная документация. Объяснение, почему nan находится в списке, содержащем его же. Справочник даёт точную эквивалентность: «For container types such as list, tuple, set, frozenset, dict, or collections.deque, the expression x in y is equivalent to any(x is e or x == e for e in y)» (Для контейнерных типов, таких как list, tuple, set, frozenset, dict или collections.deque, выражение x in y эквивалентно any(x is e or x == e for e in y)). Тождество проверяется ПЕРВЫМ — это записано прямо в порядке операндов or.https://docs.python.org/3.14/reference/expressions.html#membership-test-operations
  5. sys.intern — таблица интернированных строкОфициальная документация. Что делает функция и зачем: «Enter string in the table of “interned” strings and return the interned string — which is string itself or a copy» (Помещает строку в таблицу „интернированных“ строк и возвращает интернированную строку — саму эту строку либо её копию), и там же о выгоде: «the key comparisons (after hashing) can be done by a pointer compare instead of a string compare» (сравнение ключей (после хеширования) может выполняться сравнением указателей вместо сравнения строк). Оттуда же факт, объясняющий половину наблюдений урока: «Normally, the names used in Python programs are automatically interned» (Обычно имена, используемые в программах на Python, интернируются автоматически).https://docs.python.org/3.14/library/sys.html#sys.intern
  6. PEP 683 — Immortal Objects, Using a Fixed RefcountPEP. Эрик Сноу и Эдди Элизондо, принят в 3.12. Отсюда взято, что None, True, False и малые числа перестают считать ссылки: у вечного объекта счётчик не меняется вовсе, и sys.getrefcount возвращает служебную константу. На проверку тождества это не влияет никак — PEP нужен ровно затем, чтобы объяснить, почему это число нельзя читать как «сколько ссылок».https://peps.python.org/pep-0683/
  7. Что нового в Python 3.8 — предупреждение об is с литераломОфициальная документация. Источник версии для предупреждения и его формулировка причины: «The compiler now produces a SyntaxWarning when identity checks (is and is not) are used with certain types of literals (e.g. strings, numbers). These can often work by accident in CPython, but are not guaranteed by the language spec» (Компилятор теперь выдаёт SyntaxWarning, когда проверки тождества (is и is not) применяются к литералам некоторых типов (например, строкам, числам). В CPython они часто работают случайно, но спецификацией языка не гарантированы). Формулировка «работают случайно» — ровно то, о чём этот урок.https://docs.python.org/3/whatsnew/3.8.html