В питоне буквально все используют магические методы. Когда пишем
конструктор класса -- называем его __init__
и т.д.
Надеюсь, все умеют писать такие вещи, у меня нет желания останавливаться на основах подробней.
Поговорим о правильной перегрузке математических операций.
Создаем класс-точку
Итак, имеем точку в двухмерном пространстве:
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return 'Point({}, {})'.format(self.x, self.y)
В этом варианте сразу бросается в глаза недочет: точка позволяет
изменять свои координаты. Это плохо, ведь точка с другими координатами
-- это уже другая точка. И число int
, в отличие от списка, не
позволяет изменять себя -- только создавать новые в результате
арифметических операций.
Обновленная версия:
class Point(object):
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
Как взрослые люди мы используем джентельменское соглашение, по которому внешний пользователь класса не должен обращаться к именам, начинающимся с подчеркивания.
Над точками нужно производить какие-то операции. Самая, наверное, распространенная -- это сравнение.
Сравнение
class Point:
# ...
def __eq__(self, other):
return self._x == other._x and self._y == other._y
Что плохо? То, что попытка сравнить точку с не-точкой (Point(1, 2) == 1
) выбросит исключение AttributeError
:
>>> Point(1, 2) == 1
AttributeError: 'int' object has no attribute '_x'
в то время как стандартные питоновские типы ведут себя иначе:
>>> 1 == 'a'
False
Меняем сравнение:
def __eq__(self, other):
if not isinstance(other, Point):
return False
return self._x == other._x and self._y == other._y
Теперь сравнивание работает почти правильно:
>>> Point(1, 2) == Point(1, 2)
True
>>> Point(1, 2) == 1
False
Слово почти я употребил потому, что Питон работает так:
- сначала пытается сделать сравнение
a == b
- если сравнение не дает результата -- делается вторая попытка с
перестановкой операторов
b == a
Чтобы сказать, что операция сравнения не дает результата -- нужно
вернуть константу NotImplemented
(не путать с исключением
NotImplementedError
):
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self._x == other._x and self._y == other._y
В паре с ==
всегда идет оператор !=
, не нужно про него забывать:
def __ne__(self, other):
return not (self == other)
На самом деле Питон будет сам использовать метод __eq__
если
__ne__
не определен, но я считаю что лучше и понятней написать
__ne__
самому, тем более что это не трудно.
hash
Наши точки сравниваются, всё прекрасно. Но если мы захотим, скажем, использовать их как ключи в словаре -- получим ошибку:
>>> {Point(1, 2): 0}
TypeError: unhashable type: 'Point'
Нужно определить метод __hash__
. Питон прекрасно умеет считать хэш для кортежа, чем мы и воспользуемся:
def __hash__(self):
return hash((self._x, self._y))
Результат:
>>> {Point(1, 2): 0}
{Point(1, 2): 0}
Определять только __hash__
без __eq__
/__ne__
неправильно: в
случае коллизии задействуются операторы сравнения. Если они не
определены -- можно получить некорректный результат.
Упорядочивание
Как говорил один преподаватель, не используйте слово "сортировка" -- оно очень созвучно слову "сортир".
Точки на плоскости не имеют естественного порядка. Поэтому
реализовывать операторы упорядочивания (<
, >
, <=
, >=
) не
нужно.
Портрет разработчика, который пытается реализовывать операции не имеющие чёткого смысла в терминах предметной области, нужно вешать на доску позора.
Если для какой-то цели вы придумали принцип упорядочивания для точек на плоскости -- сделайте это нормальным методом класса со своим именем, не нужно вводить в изумление пользователей.
Арифметика
Точки можно складывать и вычитать.
def __add__(self, other):
if not isinstance(other, Point):
return NotImplemented
return Point(self._x + other._x, self._y + other._y)
def __sub__(self, other):
if not isinstance(other, Point):
return NotImplemented
return Point(self._x - other._x, self._y - other._y)
Пример:
>>> Point(1, 2) + Point(2, 3)
Point(3, 5)
>>> Point(1, 2) - Point(2, 3)
Point(-1, -1)
Так как точки неизменяемые, то возвращается новый объект.
Вообще оператор +
подразумевает, что мы создаем в качестве
результата что-то новое, а не меняем какой-то из аргументов.
Как и для сравнения, если не знаем что делать -- возвращаем NotImplemented
.
Тогда Питон попробует переставить аргументы местами, но вызовет уже __radd__
:
res = a.__add__(b)
if res is NotImplemented:
res = b.__radd__(a)
Реализуем и эти методы:
def __radd__(self, other):
if not isinstance(other, Point):
return NotImplemented
return other + self
def __rsub__(self, other):
if not isinstance(other, Point):
return NotImplemented
return other - self
Зачем это нужно? Допустим, мы хотим складывать наши точки с QPoint
из библиотеки PyQt
, полуая в результате опять объекты класса
Point
.
Тогда нужно расширить наши __add__
и __radd__
:
def __add__(self, other):
if isinstance(other, Point):
return Point(self._x + other._x, self._y + other._y)
elif isinstance(other, QPoint):
return Point(self._x + other.x(), self._y + other.y())
return NotImplemented
def __radd__(self, other):
if isinstance(other, Point):
return Point(self._x + other._x, self._y + other._y)
elif isinstance(other, QPoint):
return Point(self._x + other.x(), self._y + other.y())
return NotImplemented
Реализацию __iadd__
/__isub__
рассматривать не буду, там всё
очевидно. К тому же Питон сам способен сделать как надо,
сконструировав нужный код на основе вызовов __add__
/__sub__
.
Умножение и деление для точек не имеют смысла, поэтому их просто не делаем. Если бы делали, скажем, операции над векторами -- ввели бы скалярное и векторное произведения. "Просто точкам" эти излишества не нужны.
Заключение
Вот и всё, набросок для класса точки готов.
Надеюсь, хотя бы некоторым читателям написанное окажется полезным.
еще про перегрузку https://pythononline.ru/question/kak-peregruzit-metod-python
ОтветитьУдалитьА как сложить Pont(2, 5) + 3? Чтобы не было unsupported operand type(s) for +: 'Point' and 'int'
ОтветитьУдалить