Объектно-ориентированное программирование. Классы и объекты
Сегодня мы поговорим об объектно-ориентированном программировании и о его применении в python.
Объектно-ориентированное программирование (ООП) — парадигма программирования, в которой основными концепциями являются понятия объектов и классов.
Класс — тип, описывающий устройство объектов. Объект — это экземпляр класса. Класс можно сравнить с чертежом, по которому создаются объекты.
Python соответствует принципам объектно-ориентированного программирования. В python всё является объектами — и строки, и списки, и словари, и всё остальное.
Но возможности ООП в python этим не ограничены. Программист может написать свой тип данных (класс), определить в нём свои методы.
Это не является обязательным — мы можем пользоваться только встроенными объектами. Однако ООП полезно при долгосрочной разработке программы несколькими людьми, так как упрощает понимание кода.
Приступим теперь собственно к написанию своих классов на python. Попробуем определить собственный класс:
Теперь мы можем создать несколько экземпляров этого класса:
File
Классу возможно задать собственные методы:
И напоследок еще один пример:
Для вставки кода на Python в комментарий заключайте его в теги
- Модуль csv - чтение и запись CSV файлов
- Создаём сайт на Django, используя хорошие практики. Часть 1: создаём проект
- Онлайн-обучение Python: сравнение популярных программ
- Книги о Python
- GUI (графический интерфейс пользователя)
- Курсы Python
- Модули
- Новости мира Python
- NumPy
- Обработка данных
- Основы программирования
- Примеры программ
- Типы данных в Python
- Видео
- Python для Web
- Работа для Python-программистов
- Сделай свой вклад в развитие сайта!
- Самоучитель Python
- Карта сайта
- Отзывы на книги по Python
- Реклама на сайте
Сравнение объектов на python
Есть 2 объекта. Поэлементное сравнение обеспечивает равенство (т.е. сравнение каждого элемента с каждым, x.min_x с y.min_x , x.min_y с x.min_y и т.д.). Если сравнить на равенство с помощью оператора == , тогда возникнет они не будут равными. Все действия проводятся в рамках pyCharm
x = Rectangle(min_x=1, max_x=4, min_y=0, max_y=3) y = Rectangle(min_x=1, max_x=4, min_y=0, max_y=3) a = x.max_y == y.max_y # True a = x.max_x == y.max_x # True a = x.min_x == y.min_x # True a = x.min_y == y.min_y # True a = x == y # False
Как решить проблему?
Отслеживать
hedgehogues
задан 22 авг 2017 в 15:14
hedgehogues hedgehogues
9,579 10 10 золотых знаков 50 50 серебряных знаков 110 110 бронзовых знаков
У вас не списки, а объекты же, нет?
22 авг 2017 в 15:16
@Flowneee поправил. Спасибо
22 авг 2017 в 15:18
И? У вас все еще 2 объекта. То, что внутри они представлены списком или чем либо еще, не отменяет того, что это 2 объекта. А по умолчанию 2 объекта сравниваются никак не по содержимому, а по ссылкам на эти объекты.
22 авг 2017 в 15:22
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
Python не умеет сравнивать пользовательские объекты по умолчанию — ведь действительно, изнутри классы могут быть устроены намного сложнее, чем прямолинейные наборы значений. Их сравнение в таком случае может включать в себя сопоставление не всех переменных, а только некоторых, а иногда с ними для этого ещё нужно совершить какие-то действия. И это не говоря о том, что иногда нам может потребоваться сравнивать объекты разных классов.
Так что в общем случае два различных экземпляра пользовательских классов всегда будут считаться неравными друг другу. Чтобы проверить это, сначала определим класс для наших объектов:
class Rectangle(object): def __init__(self, min_x, min_y, max_x, max_y): self.min_x, self.min_y = min_x, min_y self.max_x, self.max_y = max_x, max_y
Попробуем на всякий случай сравнить два произвольных объекта, не имеющих ничего общего:
foo = Rectangle(0, 0, 42, 42) bar = Rectangle(4, 8, 15, 16) print(foo == bar) # False
Пока что, в общем-то, ничего неожиданного. Теперь посмотрим, что получится при сравнении экземпляров, обладающих идентичными наборами атрибутов:
baz = Rectangle(0, 0, 42, 42) # казалось бы, этот объект ничем не отличается от первого print(foo == baz) # False
Вопреки ожиданиям, при попытке сравнения мы всё ещё получаем False — оператор == даже не пытается смотреть на атрибуты объектов.
Кажется, что с чем бы мы не пытались сравнить экземпляр пользовательского класса, результат всегда будет отрицательным. Это близко к правде, но с одним небольшим исключением:
print(foo == foo) # True
Каждый объект по умолчанию равен сам себе — то есть, вообще говоря, функция сравнения полностью эквивалентна проверке object1 is object2 . Этим можно пользоваться, хотя всё-таки лучше делать это напрямую через is .
Ну и если вспомнить специфику присваивания в питоне, становится понятно, как работает следующий случай:
quux = foo print(quux == foo, quux is foo) # True True
Мы не создали новый объект, а просто привязали имя quux к старому объекту foo, который как был один, так и остался. И сам себе он, как и раньше, эквивалентен.
Теперь о том, как эту ситуацию можно изменить.
Возможное решение «в лоб» — взять словари всех атрибутов объектов и сравнить их напрямую — а словари считаются равными тогда и только тогда, когда все их ключи и значения попарно равны:
print(bar.__dict__ == baz.__dict__) # True
Однако подобный прямой доступ к служебным переменным извне без веского повода считается очень некрасивым и «непитоновским», тем более, что полностью это проблему не решает — если среди значений атрибутов будут не только простые сущности, но и, например, экземпляры вложенных классов, сравнение снова не сработает как надо. Да и запись такого способа очень неудобная и громоздкая сама по себе.
«Правильное» решение в подобных случаях — определить в классе метод __eq__ , который задаёт взаимодействие объектов с оператором == . Метод должен принимать на вход два аргумента self и other и возвращать True , если объекты равны и False — если они не равны. Если объекты таких типов вообще не сравнимы друг с другом, нужно возвращать NotImplemented — увидев его, интерпретатор попробует использовать метод __eq__ из второго аргумента, ну а если и он вернёт NotImplemented , результат сведётся к False .
Переопределим наш класс так, чтобы при вызове __eq__ попарно сравнивались координаты прямоугольников:
class Rectangle(object): . def __eq__(self, other): # сравнение двух прямоугольников if isinstance(other, Rectangle): return (self.min_x == other.min_x and self.min_y == other.min_y and self.max_x == other.max_x and self.max_y == other.max_y) # иначе возвращаем NotImplemented return NotImplemented foo = Rectangle(0, 0, 42, 42) bar = Rectangle(4, 8, 15, 16) print(foo == bar) # всё ещё False baz = Rectangle(0, 0, 42, 42) print(foo == baz) # True print(foo == 10) # False
Метод __eq__ добавляет нашему классу немало гибкости — объекты могут иметь атрибуты, не участвующие в процессе сравнения (к примеру, уникальные имена), да и сам алгоритм при необходимости можно усложнять: скажем, у прямоугольников можно сравнивать только форму, вне зависимости от расположения на плоскости.
class Rectangle(object): def __init__(self, min_x, min_y, max_x, max_y, name=None): . self.name = name def __eq__(self, other): if isinstance(other, Rectangle): return (self.max_x - self.min_x == other.max_x - other.min_x and self.max_y - self.min_y == other.max_y - other.min_y) return NotImplemented print(Rectangle(4, 8, 15, 16, 'spam') == Rectangle(14, 18, 25, 26, 'eggs')) # True
Или и вовсе можно сравнивать прямоугольники только по площади, причём не только друг с другом, но и, скажем, с треугольниками или даже с числовыми значениями:
from math import sqrt class Rectangle(object): . def area(self): return (self.max_x - self.min_x)*(self.max_y - self.min_y) def __eq__(self, other): if isinstance(other, (Rectangle, Triangle)): return self.area() == other.area() if isinstance(other, (int, float)): return self.area() == other return NotImplemented class Triangle(object): def __init__(self, a_x, a_y, b_x, b_y, c_x, c_y): # задаётся координатами трёх вершин self.a_x, self.a_y = a_x, a_y self.b_x, self.b_y = b_x, b_y self.c_x, self.c_y = c_x, c_y # сразу посчитаем длины сторон self.ab = sqrt((a_x - b_x)**2 + (a_y - b_y)**2) self.bc = sqrt((b_x - c_x)**2 + (b_y - c_y)**2) self.ca = sqrt((c_x - a_x)**2 + (c_y - a_y)**2) def area(self): p = (self.ab + self.bc + self.ca)/2 return sqrt(p * (p - self.ab) * (p - self.bc) * (p - self.ca)) def __eq__(self, other): . print(Rectangle(57, 179, 59, 185) == Rectangle(-100, 4, -96, 7)) # True print(Rectangle(1, 4, 9, 7) == Triangle(0, 0, 0, 6, 8, 0)) # True
Строго говоря, для треугольников можно было вообще не определять метод __eq__ — для межклассового сравнения достаточно того, чтобы он присутствовал хоть в каком-то из классов. Но в общем случае всё равно стоит прописывать его и там, и там. Во-первых, это позволит, если понадобится, сравнивать треугольники между собой. Во-вторых, при обработке сравнения интерпретатор может в первую очередь попытаться использовать __eq__ как из первого объекта, так и из второго (зависит от конкретных параметров интерпретации), и наличие обоих методов немного ускорит обработку.
Вот как-то так. Подробнее про __eq__ , обратный ему __ne__ и прочие специальные классовые методы можно почитать в документации.
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)