Как сравнивать классы в python
Перейти к содержимому

Как сравнивать классы в python

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

В этом кратком и практическом руководстве вы узнаете, когда использовать операторы Python is, is not, == и !=. Вы увидите, что эти операторы сравнения делают под капотом, погрузитесь в некоторые особенности идентификации объекта и интернирования, а также определите пользовательский класс.

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

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

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

  • В чём разница между равенством объектов и идентичностью.
  • Когда использовать операторы равенства и идентичности для сравнения объектов.
  • Что делают эти операторы Python под капотом.
  • Почему использование is и is not для сравнения значений приводит к неожиданному поведению.
  • Как написать собственный метод класса __eq__() для определения поведения оператора равенства.

Сравнение идентичности с операторами 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 == и != для сравнения равенства объектов. Здесь вы обычно сравниваете значения двух объектов. Это то, что вам нужно, если вы хотите сравнить, имеют ли два объекта одинаковое содержимое, и вам всё равно, где они хранятся в памяти.
  • Используйте операторы Python is и is not , если хотите сравнить идентификатор объекта. Здесь вы сравниваете, указывают ли две переменные на один и тот же объект в памяти. Основной вариант использования этих операторов — сравнение с None . Сравнивать с None по адресу памяти быстрее и безопаснее, чем с помощью методов класса.

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

Заключение

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

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

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

Методы сравнений __eq__, __ne__, __lt__, __gt__ и другие

Но здесь объекты сравниваются по их id (адресу в памяти), а мы бы хотели, чтобы сравнивались секунды в каждом из объектов c1 и c2. Для этого переопределим магический метод __eq__(), следующим образом:

def __eq__(self, other): if not isinstance(other, (int, Clock)): raise TypeError("Операнд справа должен иметь тип int или Clock") sc = other if isinstance(other, int) else other.seconds return self.seconds == sc

Теперь, после запуска программы видим значение True, т.к. объекты содержат одинаковое время. Кроме того, мы можем совершенно спокойно выполнять проверку и на неравенство:

print(c1 != c2)

Смотрите, если интерпретатор языка Python не находит определение метода ==, то он пытается выполнить противоположное сравнение с последующей инверсией результата. То есть, в данном случае находится оператор == и выполняется инверсия: not (a == b) Давайте в этом убедимся, поставим точку останова в метод __eq__ и запустим программу. Как видите, он срабатывает и результат в последствии меняется на противоположный. Отлично, на равенство и неравенство мы теперь можем сравнивать объекты класса Clock, а также с целыми числами. Однако, сравнение на больше или меньше пока не работает. Строчка программы:

print(c1  c2)

приведет к ошибке. Добавим эту операцию сравнения:

def __lt__(self, other): if not isinstance(other, (int, Clock)): raise TypeError("Операнд справа должен иметь тип int или Clock") sc = other if isinstance(other, int) else other.seconds return self.seconds  sc

Как видите, у нас здесь получается дублирование кода. Поэтому, я вынесу общее для методов сравнения в отдельный метода класса:

@classmethod def __verify_data(cls, other): if not isinstance(other, (int, Clock)): raise TypeError("Операнд справа должен иметь тип int или Clock") return other if isinstance(other, int) else other.seconds

А сами методы примут вид:

def __eq__(self, other): sc = self.__verify_data(other) return self.seconds == sc def __lt__(self, other): sc = self.__verify_data(other) return self.seconds  sc

Итак, мы определили сравнение на равенство и меньше. Теперь, можно сравнивать объекты класса Clock на эти операции и дополнительно на неравенство и больше. Сейчас команда:

c1 = Clock(1000) c2 = Clock(2000) print(c1  c2)

Выдаст True, так как первое время меньше второго. И также мы можем совершенно спокойно делать проверку на больше:

print(c1 > c2)

Здесь сработает тот же метод меньше, но для объекта c2: c2 < c1 То есть, в отличие от оператора ==, где применяется инверсия, здесь меняется порядок операндов. Разумеется, если в классе определен метод больше:

def __gt__(self, other): sc = self.__verify_data(other) return self.seconds > sc

то он будет найден и выполнен. Подмена происходит только в случае отсутствия соответствующего магического метода. И то же самое для методов сравнения на меньше или равно и больше или равно:

def __le__(self, other): sc = self.__verify_data(other) return self.seconds  sc

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

print(c1  c2)

то он сработает и результат отобразится в консоли. Но, если пропишем обратное сравнение:

print(c1 >= c2)

Сравнение экземпляров класса

Вот задача:
Ну и как их прикажете называть? Кота и Лису? Ну не честными сеньорами! Разбойники — самое подходящее для них имя!

Напишите класс Разбойник с большой дороги (Highwayman). Экземпляр h класса инициализируется с аргументами: имя, свойство (слепой, хромой, глупый) (blind, lame, stupid etc.), количество обманутых. Класс реализует (инкапсулирует) функциональность:

str(h) — возвращать свое имя в виде Highwayman — ;
h.lie() — обманывать. Удается обмануть не всегда, через раз, первый раз попытка неудачная, возвращается False. В следующий раз попытка будет удачной, возвращается True. В случае удачи увеличивается количество обманутых у этого разбойника;
h.boast() — возвращать хвастливую строку Uhaha столько раз, сколько обманутых у этого разбойника;
h.change_property(property) — менять свойство при вызове метода на значение аргумента;
Экземпляры класса можно сравнивать. Сначала сравнивается количество обманутых, потом длина свойства, потом длина имени, потом имена в алфавитном порядке.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
class Highwayman: def __init__(self, name, property, cheat): self.name = name self.property = property self.cheat = cheat self.test = 0 def __str__(self): return "Highwayman " + self.name + ' - ' + self.property def lie(self): if self.test == 0: self.test = 1 return False else: self.test = 0 self.cheat += 1 return True def boast(self): return 'Uhaha' * self.cheat def change_property(self, property): self.property = property h1 = Highwayman('Cat', 'blind', 2) h2 = Highwayman('Fox', 'lame', 2) print(h2) print(h1 > h2) h2.change_property('stupid') print(h1 > h2) h1.lie() h1.lie() print(h1.boast(), h2.boast(), sep='\n') print(h1 > h2)

Вот тест:
h1 = Highwayman(‘Cat’, ‘blind’, 2)
h2 = Highwayman(‘Fox’, ‘lame’, 2)
print(h2)
print(h1 > h2)
h2.change_property(‘stupid’)
print(h1 > h2)
h1.lie()
h1.lie()
print(h1.boast(), h2.boast(), sep=’\n’)
print(h1 > h2)
Вывод:
Highwayman Fox — lame
True
False
UhahaUhahaUhaha
UhahaUhaha
True

Как сравнивать объекты в Python

В Python существует несколько операторов сравнения, которые позволяют сравнивать объекты разных типов:

Примеры использования операторов сравнения:

a = 10 b = 20 print(a == b) # False print(a != b) # True print(a < b) # True print(a > b) # False print(a <= b) # True print(a >= b) # False

Сравнение строк

Строки в Python сравниваются лексикографически, то есть посимвольно с использованием кодировки Unicode.

Пример сравнения строк:

string1 = "apple" string2 = "banana" print(string1 == string2) # False print(string1 < string2) # True

Сравнение пользовательских объектов

При создании пользовательских классов, вы можете определить специальные методы для сравнения объектов этого класса.

Пример определения специальных методов для сравнения объектов класса Person :

class Person: def __init__(self, name, age): self.name = name self.age = age def __eq__(self, other): if isinstance(other, Person): return self.name == other.name and self.age == other.age return False def __ne__(self, other): return not self == other person1 = Person("Alice", 30) person2 = Person("Bob", 35) person3 = Person("Alice", 30) print(person1 == person2) # False print(person1 == person3) # True print(person1 != person2) # True

Python-разработчик: новая работа через 9 месяцев
Получится, даже если у вас нет опыта в IT

Использование функции cmp()

В Python 2 существовала встроенная функция cmp() , которая сравнивала два объекта и возвращала -1 , если первый объект меньше второго, 0 — если объекты равны, и 1 — если первый объект больше второго. В Python 3 эта функция была удалена, но вы можете создать свою собственную функцию cmp() .

Пример создания и использования функции cmp() :

def cmp(a, b): if a < b: return -1 elif a == b: return 0 else: return 1 x = 5 y = 10 print(cmp(x, y)) # -1

В заключение, сравнение объектов в Python может быть выполнено с помощью операторов сравнения, специальных методов для пользовательских классов и функции cmp() . Эти подходы позволяют гибко и эффективно сравнивать объекты различных типов. Удачи вам в изучении Python! ��

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *