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

Источник: «Python '!=' Is Not 'is not': Comparing Objects in Python»
В этом кратком и практическом руководстве вы узнаете, когда использовать операторы Python is, is not, == и !=. Вы увидите, что эти операторы сравнения делают под капотом, погрузитесь в некоторые особенности идентификации объекта и интернирования, а также определите пользовательский класс.

Между оператором тождества Python is и оператором равенства == есть тонкое различие. Ваш код может работать нормально, если вы используете оператор Python is для сравнения чисел, пока он вдруг не перестанет работать. Возможно вы где-то слышали, что оператор Python is работает быстрее, чем оператор ==, или может показаться, что он выглядит более питоническим (pythonic). Однако важно помнить, что эти операторы ведут себя по-разному.

Оператор == сравнивает значение или равенство двух объектов, тогда как оператор Python is проверяет, указывают ли две переменные на один и тот же объект в памяти. В подавляющем большинстве случаев это означает, что вы должны использовать операторы равенства == и !=, за исключением случаев, когда вы сравниваете с None.

В этом руководстве вы узнаете:

Сравнение идентичности с операторами is и is not

Операторы Python is и is not сравнивают идентичность двух объектов. В CPython это их адрес памяти. В Python всё является объектом, и каждый объект хранится в определённом месте памяти. Операторы Python is и is not проверяют, ссылаются ли две переменные на один и тот же объект в памяти.

Примечание. Имейте в виду, что объекты с одинаковыми значениями обычно хранятся по разным адресам памяти.

Вы можете использовать id() для проверки идентичности объекта:

>>> help(id)
Help on built-in function id in module builtins:

id(obj, /)
Return the identity of an object.

This is guaranteed to be unique among simultaneously existing objects.
(CPython uses the object's memory address.)

>>> id(id)
2570892442576

Последняя строка показывает адрес памяти, где храниться встроенная функция id.

Есть несколько распространённых случаев, когда объекты с одинаковым значением будут иметь одинаковый идентификатор по умолчанию. Например, числа от -5 до 256 интернированы в CPython. Каждое число храниться в единственном и фиксированном месте памяти, что экономит память для часто используемых целых чисел.

Вы можете использовать sys.intern() для интернирования строк и повышения производительности. Эта функция позволяет сравнивать их адреса памяти, а не сравнивать строки посимвольно:

>>> from sys import intern
>>> a = 'hello world'
>>> b = 'hello world'
>>> a is b
False
>>> id(a)
1603648396784
>>> id(b)
1603648426160

>>> a = intern(a)
>>> b = intern(b)
>>> a is b
True
>>> id(a)
1603648396784
>>> id(b)
1603648396784

Переменные a и b изначально указывают на два разных объекта в памяти, о чём свидетельствуют их разные идентификаторы. Когда вы интернируете их, вы гарантируете, что a и b указывают на один и тот же объект в памяти. Любая новая строка со значением hello world теперь будет создаваться в новой ячейке памяти, но когда вы интернируете это новую строку, вы убедитесь, что она указывает на тот же адрес памяти, что и первый hello world, который вы интернировали.

Примечание. Несмотря на то, что адрес памяти объекта уникален в любой момент времени, он различается между запусками одного и того же кода и зависит от версии CPython и компьютера, на котором он выполняется.

Другими интернированными по умолчанию объектами являются None, True, False и простые строки. Имейте в виду, что в большинстве случаев разные объекты с одинаковым значением будут храниться по разным адресам памяти. Это означает, что вы не должны использовать оператор Python is для сравнения значений.

Когда только некоторые целые числа интернированы

За кулисами Python интернирует объекты с часто используемыми значениями (например, целыми числами от -5 до 256) для экономии памяти. Следующий фрагмент кода показывает, что некоторые целые числа имеют фиксированный адрес памяти:

>>> a = 256
>>> b = 256
>>> a is b
True
>>> id(a)
1638894624
>>> id(b)
1638894624

>>> a = 257
>>> b = 257
>>> a is b
False

>>> id(a)
2570926051952
>>> id(b)
2570926051984

Первоначально a и b указывают на один и тот же интернированный объект в памяти, но когда их значения выходят за пределы диапазона обычных целых чисел (от -5 до 256), они сохраняются по разным адресам памяти.

Когда несколько переменных указывают на один и тот же объект

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

>>> a = [1, 2, 3]
>>> b = a
>>> a
[1, 2, 3]
>>> b
[1, 2, 3]

>>> a.append(4)
>>> a
[1, 2, 3, 4]
>>> b
[1, 2, 3, 4]

>>> id(a)
2570926056520
>>> id(b)
2570926056520

Что сейчас произошло? Вы добавляете новый элемент в a, но теперь b тоже содержит этот элемент! Что ж, в строке где b = a, вы указываете переменной b так, что бы она указывала на тот же адрес памяти, что и a, так что теперь обе переменные ссылаются на один и тот же объект.

Если вы определяете эти списки независимо друг от друга, то они хранятся по разным адресам памяти и ведут себя независимо:

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b
False
>>> id(a)
2356388925576
>>> id(b)
2356388952648

Поскольку a и b теперь относятся к разным объектам в памяти, изменение одного не влияет на другой.

Сравнения равенства с операторами Python == и !=

Напомним, что объекты с одинаковым значением часто хранятся по разным адресам памяти. Используйте операторы равенства == и !=, если хотите проверить, имеют ли два объекта одинаковое значение, независимо от того, где они хранятся в памяти. В подавляющем большинстве случаев это то, что вы хотите сделать.

Когда копия объекта равна, но не идентична

В приведённом ниже примере вы устанавливаете b как копию a (которая является мутабельным объектом, таким как список или словарь). Обе переменные будут иметь одинаковое значение, но каждая будет храниться по разным адресам памяти:

>>> a = [1, 2, 3]
>>> b = a.copy()
>>> a
[1, 2, 3]
>>> b
[1, 2, 3]

>>> a == b
True
>>> a is b
False

>>> id(a)
2570926058312
>>> id(b)
2570926057736

a и b хранятся по разным адресам памяти, поэтому a is b не будет возвращать True. Однако a == b возвращает True, поскольку оба объекта имеют одинаковое значение.

Как работает сравнение по равенству

Магия оператора равенства == происходит в методе __eq__() класса объекта слева от знака ==.

Примечание. Это так, если только объект справа не является подклассом объекта слева. Для получения дополнительно информации посмотрите официальную документацию.

Это магический метод класса, который вызывается всякий раз, когда экземпляр этого класса сравнивается с другим объектом. Если этот метод не реализован, то == по умолчанию сравнивает адреса памяти двух объектов.

В качестве упражнения создайте класс SillyString, наследуемый от str, и реализуйте __eq__(), чтобы сравнить, совпадает ли длина этой строки с длиной другого объекта:

class SillyString(str):
# Этот метод вызывается при использовании == на объекте
def __eq__(self, other):
print(f'comparing {self} to {other}')
# Вернуть True, если self и other имеют одинаковую длину
return len(self) == len(other)

Теперь SillyString hello world должен быть равен строке world hello, и даже другому объекту такой же длины:

>>> # Сравнение двух строк
>>> 'hello world' == 'world hello'
False

>>> # Сравнение строки с SillyString
>>> 'hello world' == SillyString('world hello')
comparing world hello to hello world
True

>>> # Сравнение SillyString со списком
>>> SillyString('hello world') == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
comparing hello world to [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
True

Это, конечно, глупое поведение для объекта, который переопределяет поведение строки, но он иллюстрирует, что происходит, когда вы сравниваете два объекта, используя ==. Оператор != даёт ответ, обратный этому, если только не реализован конкретный метод класса __ne__().

Приведённый выше пример также ясно показывает, почему рекомендуется использовать оператор Python is для сравнения с None вместо оператора ==. Он не только быстрее, поскольку сравнивает адреса памяти, но и безопаснее, поскольку не зависит от логики каких либо методов __eq__() класса.

Сравнение операторов сравнения Python

Как правило, вы всегда должны использовать операторы равенства == и !=, за исключением случаев, когда сравниваете с None:

Переменные с одним и тем же значением часто хранятся по разным адресам памяти. Это означает, что вы должны использовать == и != для сравнения их значений и использовать операторы Python is и is not только тогда, когда вы хотите проверить, указывают ли две переменные на один и тот же адрес памяти.

Заключение

Из этого руководства вы узнали, что == и != сравнивают значения двух объектов, тогда как операторы is и is not сравнивают, ссылаются ли дак переменные на один и тот же объект в памяти. Если вы будете помнить об этом различии, вы сможете предотвратить неожиданное поведение в своём коде.

Если вы хотите узнать больше о прекрасном мире интернированных объектов и об операторе Python is, ознакомьтесь со статьёй Why you should almost never use “is” in Python. Вы также сможете посмотреть, как можно использовать sys.intern() для оптимизации использования памяти и времени сравнения строк, хотя есть вероятность, что Python уже автоматически обрабатывает это за вас за кулисами.

Теперь, когда вы узнали, что под капотом делают операторы равенства и идентичности, вы можете попробовать написать свои собственные методы __eq__() класса, определяющие, как экземпляры этого класса сравниваются при использовании оператора ==. Идите и примените свои новые знания об этих операторах сравнения Python!

Дополнительные материалы

Предыдущая Статья

Laravel: Моделирование бизнес процессов

Следующая Статья

Python: Понимание объекта NoneType