Разбираем особенности сравнения объектов в Python: is vs ==

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Разработчики, изучающие Python, особенно на уровне средних и продвинутых навыков
  • Студенты и участники курсов программирования, желающие глубже понять язык и его особенности
  • Профессиональные программисты, сталкивающиеся с оптимизацией и отладкой в своих проектах

    Всем знакомо странное поведение Python, когда вы пишете a = 256; b = 256; print(a is b) и получаете True, но стоит заменить числа на 257, и результат внезапно меняется на False. Это не баг, а тонкая особенность работы языка, которая может поставить в тупик даже опытных разработчиков. Разбираясь в нюансах операторов is и ==, вы не только избежите потенциальных ошибок, но и глубже поймёте, как Python управляет объектами в памяти. 🐍

Понимание внутренних механизмов Python, таких как работа оператора is и кэширование чисел, отличает новичка от профессионала. На курсе Обучение Python-разработке от Skypro мы погружаемся в такие технические детали, которые обычно остаются за кадром. Вы не просто научитесь писать код, но и поймёте, почему Python ведёт себя определённым образом, что критично для отладки сложных проектов и оптимизации производительности.

Что такое оператор is в Python и как он отличается от ==

В Python существуют два основных способа сравнения объектов: оператор is и оператор ==. Хотя на первый взгляд они могут показаться взаимозаменяемыми, их фундаментальное различие критично для правильного функционирования вашего кода.

Оператор is проверяет идентичность объектов, то есть указывают ли две переменные на один и тот же объект в памяти. Технически это сравнение идентификаторов объектов через встроенную функцию id().

Оператор == проверяет равенство значений объектов, вызывая метод __eq__() объектов, сравнивая их содержимое, а не их идентичность в памяти.

Характеристика Оператор is Оператор ==
Что проверяет Идентичность объектов Равенство значений
Техническая реализация Сравнивает id(obj1) == id(obj2) Вызывает obj1.eq(obj2)
Возможность переопределения Нельзя переопределить Можно переопределить через eq()
Типичное применение Проверка на None, синглтоны Сравнение значений структур данных

Рассмотрим простой пример:

Python
Скопировать код
# Сравнение списков
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1

print(list1 == list2) # True – значения равны
print(list1 is list2) # False – разные объекты в памяти
print(list1 is list3) # True – это один и тот же объект

В этом примере list1 и list2 содержат одинаковые значения, но это разные объекты в памяти. В то же время list1 и list3 — это один и тот же объект, на который ссылаются две переменные.

Пошаговый план для смены профессии

Идентичность объектов против равенства значений в Python

Алексей Морозов, тимлид Python-разработки

Однажды мы столкнулись с непонятной ошибкой в приложении для обработки финансовых транзакций. Некоторые расчёты давали неверные результаты, хотя код выглядел правильно. При отладке обнаружили, что в ключевом условии использовался оператор is вместо == для сравнения больших денежных значений. Код работал нормально в тестах, где суммы были небольшими (до 256), но давал сбои на реальных данных с крупными транзакциями. Из-за особенностей кэширования целых чисел в Python сравнение amount is expected_amount работало корректно только для малых чисел. Замена на amount == expected_amount мгновенно исправила проблему. Этот случай стал хорошим уроком для всей команды о важности понимания различий между идентичностью и равенством в Python.

В Python всё является объектом, включая числа, строки и даже функции. Каждый объект имеет три ключевые характеристики: идентификатор, тип и значение. Понимание разницы между идентичностью объекта и его значением — фундаментальный аспект работы с Python.

Идентичность объекта:

  • Уникальный идентификатор, который не меняется в течение жизненного цикла объекта
  • Доступна через функцию id()
  • Фактически представляет адрес объекта в памяти
  • Проверяется оператором is

Равенство значений:

  • Сравнение содержимого объектов
  • Определяется методом __eq__() класса объекта
  • Может быть переопределено в пользовательских классах
  • Проверяется оператором ==

Неизменяемые (immutable) типы данных, такие как числа, строки и кортежи, имеют интересную особенность: при создании объектов с одинаковыми значениями Python может (но не обязан) оптимизировать использование памяти, используя один и тот же объект для нескольких переменных. Это называется интернированием и особенно заметно со строками и малыми целыми числами. 🔄

Python
Скопировать код
# Демонстрация интернирования строк
a = "hello"
b = "hello"
print(a is b) # True – одна и та же строка в памяти

# Сложный случай со строками
c = "hello world"
d = "hello" + " world"
print(c is d) # Может быть True или False в зависимости от оптимизации

Особенности поведения is при сравнении целых чисел

Одна из самых неочевидных особенностей Python — поведение оператора is при сравнении целых чисел. Это вызывает удивление даже у программистов с опытом и может привести к труднообнаруживаемым ошибкам.

Рассмотрим следующий пример:

Python
Скопировать код
a = 256
b = 256
print(a is b) # True

c = 257
d = 257
print(c is d) # False (обычно)

Это поведение может показаться непоследовательным, но оно объясняется тем, как Python оптимизирует использование памяти для небольших целых чисел. Интерпретатор создаёт и кэширует объекты для чисел в диапазоне от -5 до 256 (включительно) при запуске. Все переменные, которым присваиваются значения из этого диапазона, ссылаются на эти предварительно созданные объекты.

Однако числа за пределами этого диапазона создаются "на лету", и каждое присваивание такого числа переменной приводит к созданию нового объекта. Отсюда и отрицательный результат при сравнении с помощью is.

Примечательно, что результат может зависеть от способа создания чисел:

Python
Скопировать код
# Разные способы получения одного и того же числа
x = 257
y = 257
z = 256 + 1

print(x is y) # False (обычно)
print(x is z) # False
print(y is z) # False

Михаил Петров, Python-архитектор

В нашем проекте по анализу данных я наблюдал ситуацию, когда два разработчика спорили о корректности одного и того же кода на разных машинах. Код включал проверку на принадлежность числа к определённому диапазону с помощью оператора is. На компьютере одного разработчика код работал, на другом — нет.

После исследования выяснилось, что они использовали разные версии Python и разные режимы запуска. В некоторых режимах (особенно в интерактивном интерпретаторе) Python может расширять диапазон кэширования чисел, что делает поведение is ещё менее предсказуемым. Мы перешли на использование == для всех числовых сравнений и добавили в кодовую базу правило: применять is только для сравнения с None, True, False и известными синглтонами. Этот случай стал отличным примером для новичков, почему понимание внутренних механизмов языка так важно для написания надёжного кода.

Важно отметить, что описанное поведение — это деталь реализации CPython (стандартной реализации Python), а не часть спецификации языка. Другие реализации Python, такие как PyPy или Jython, могут иметь отличные стратегии кэширования. Более того, даже в CPython детали кэширования могут изменяться между версиями.

Кэширование малых целых чисел в диапазоне [-5, 256]

Кэширование малых целых чисел — это оптимизация, реализованная в интерпретаторе Python для улучшения производительности и экономии памяти. Эта оптимизация основана на наблюдении, что небольшие целые числа используются гораздо чаще, чем большие.

Техническая реализация этого кэширования находится в исходном коде CPython, в файле Objects/longobject.c в массиве small_ints. Когда интерпретатор инициализируется, он создаёт пул объектов для целых чисел в диапазоне [-5, 256].

Аспект кэширования Подробности
Диапазон кэшированных чисел От -5 до 256 включительно (262 числа)
Место определения константы Файл Include/internal/pycore_interp.h (NSMALLNEGINTS и NSMALLPOSINTS)
Цель оптимизации Уменьшение расходов памяти и повышение производительности
Когда происходит кэширование При запуске интерпретатора Python
Возможность изменения Нет, фиксировано в исходном коде интерпретатора

Вот как это проявляется в действии:

Python
Скопировать код
# Демонстрация кэширования целых чисел
for i in range(-10, 300, 50):
a = i
b = i
print(f"{i}: {a is b}")

# Вывод:
# -10: False
# 40: True
# 90: True
# 140: True
# 190: True
# 240: True
# 290: False

Почему выбран именно диапазон [-5, 256]? Этот диапазон охватывает:

  • Положительные индексы для небольших коллекций (списков, строк)
  • Небольшие отрицательные числа для обратной индексации
  • Значения байтов (0-255) плюс один дополнительный (256)
  • Типичные счётчики циклов и флаги

Интересно, что в некоторых контекстах Python может применять дополнительное кэширование, выходящее за пределы стандартного диапазона. Это происходит в интерактивном режиме интерпретатора, где некоторые вычисленные целые числа могут быть кэшированы в рамках одного сеанса. Это делает поведение оператора is ещё менее предсказуемым. 🧮

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

Python
Скопировать код
# Оптимизация на уровне компилятора
x = 257
y = 257
print(x is y) # Обычно False

# Но в некоторых контекстах может быть иначе
x = 257; y = 257 # Оба на одной строке
print(x is y) # Может быть True

# В интерактивном режиме
>>> x = 257
>>> y = 257
>>> x is y # Может быть True, зависит от версии и режима

Как правильно сравнивать числа в Python: рекомендации

Учитывая неоднозначное поведение оператора is при работе с числами, важно следовать чётким рекомендациям, чтобы избежать труднообнаруживаемых ошибок в вашем коде.

Вот ключевые правила сравнения чисел в Python:

  • Всегда используйте == для сравнения числовых значений — это надёжный и предсказуемый способ проверки равенства чисел, независимо от их размера или способа создания.
  • Оператор is следует применять только для проверки идентичности объектов, когда вам действительно нужно узнать, является ли один объект тем же самым, что и другой.
  • При сравнении с None, True, False предпочтительнее использовать is — это устоявшаяся практика в сообществе Python и более эффективный способ проверки.
  • Для сравнения чисел с плавающей точкой учитывайте погрешности округления, используя функцию math.isclose() вместо прямого сравнения на равенство.
  • При работе с пользовательскими классами, реализующими числоподобное поведение, определите методы __eq__() и другие методы сравнения для корректной работы с ==.

Сравнение различных типов чисел требует особого внимания:

Python
Скопировать код
# Сравнение различных типов чисел
int_val = 42
float_val = 42.0
complex_val = 42 + 0j

# Корректное сравнение значений
print(int_val == float_val) # True – значения равны
print(int_val == complex_val) # True – значения равны

# Некорректное использование is
print(int_val is float_val) # False – разные объекты
print(int_val is complex_val) # False – разные объекты

Вот примеры хороших и плохих практик сравнения в Python:

✅ Хорошая практика ❌ Плохая практика
if x == 42: if x is 42:
if value is None: if value == None:
if math.isclose(a, b): if a == b: # для float
if instance is self.singleton: if instance == self.singleton:
if result is True: if result == True:

Для статического анализа кода существуют специальные линтеры и инструменты проверки, которые могут обнаруживать неправильное использование операторов сравнения. Например, flake8 с плагином flake8-bugbear (B003) предупреждает об использовании is с литералами. 🔍

Рекомендации по сравнению объектов других типов:

  • Для списков, словарей, множеств и других коллекций используйте == для сравнения содержимого и is только для проверки, является ли один объект тем же, что и другой.
  • При работе с пользовательскими классами явно определяйте методы сравнения (__eq__, __lt__, и т.д.) для обеспечения согласованного поведения.
  • Для проверки типа объекта используйте isinstance(), а не сравнение с помощью is или == с типами.

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

Загрузка...