Каждый разработчик Python, создающий (и использующий) классы, должен знать о “магических методах”.
Это те, которые начинаются и заканчиваются двойным подчеркиванием; вам известны некоторые из них, например __init()__.
Мы будем работать с несколькими из них и рассмотрим свойства, которые используются для инкапсуляции атрибутов класса.
Первые шаги
Будем учиться на примерах.
Итак, мы будем работать с классом, который я назову Time и который будет содержать три атрибута: часы, минуты и секунды.
Наша цель – выдать не конкретный час дня, а единицу времени в целом, поэтому мы допускаем значения больше 24 часов и меньше 0.
class Time: def __init__(self, h=0, m=0, s=0): self.h = h self.m = m self.s = s
Пока у нас есть первое определение нашего класса, который принимает аргументы и хранит их в экземпляре, чтобы к ним можно было обращаться как к атрибутам.
Давайте попробуем: попробуем представить 14 часов 23 минуты и 10 секунд.
a = Time(14, 23, 10)
print(a.h, a.m, a.s) # 14 23 10
Code language: PHP (php)
Аргументы имеют значение по умолчанию, равное нулю.
# Полчаса.
b = Time(m=30)
print(b.h, b.m, b.s) # 0 30 0
Code language: PHP (php)
Теперь было бы интересно иметь возможность напечатать любой экземпляр Time напрямую и заставить его показать нам три его атрибута.
Если мы сделаем это сейчас, то увидим, что результат выглядит примерно так.
print(a) # <__main__.Time object at 0x027C6430>
Code language: PHP (php)
Это ничего не говорит нам об объекте, кроме его адреса в памяти.
Давайте изменим это, определив метод __repr()__ ниже инициализатора.
def __repr__(self):
return f"<Time {self.h:02}:{self.m:02}:{self.s:02}>"
Code language: CSS (css)
Результат этого метода будет возвращен функцией str() при попытке преобразовать экземпляр нашего класса в строку, что как раз и делает print().
Поэтому, если мы сейчас распечатаем наш объект, мы увидим более четкое представление.
print(a) # <Time 14:23:10>
Code language: CSS (css)
Отлично! Мы уже реализовали наш первый магический метод.
Свойства
Свойства позволяют нам инкапсулировать атрибуты внутри класса. Для чего они нужны?
Давайте рассмотрим следующее.
a = Time(14, 23, 10)
a.m = "Привет, мир!"
print(a) # ValueError!
Code language: PHP (php)
По сути, проблема заключается в том, что ничто не мешает присвоить строку атрибуту, который должен быть целым числом.
Для решения этой проблемы мы воспользуемся встроенным декоратором property(), который позволяет нам контролировать получение и изменение значения атрибута.
Сделаем это для трех атрибутов.
Давайте начнем с времени.
Первое, что нам нужно сделать, это определить функцию, которая будет вызываться Python, когда он попытается получить доступ к значению нашего атрибута.
@property
def h(self):
return self._h
Code language: PHP (php)
Таким образом, print(a.h) выведет значение, возвращаемое методом h().
Но мы еще не определили атрибут _h, то есть значение, которое мы хотим инкапсулировать, чтобы предотвратить присвоение ему любого типа данных, кроме целого.
Итак, следующее, что нужно сделать, это определить другую функцию, которая будет вызываться, когда атрибуту h будет присвоено новое значение.
@h.setter
def h(self, value):
self._h = value
Code language: PHP (php)
Таким образом, выполнение a.h = 50 будет эквивалентно вызову вышеуказанной функции.
Это позволяет нам устанавливать ограничения на присваиваемые значения, например, проверять их тип данных.
@h.setter
def h(self, value):
if not isinstance(value, int):
raise TypeError("Необходимо ввести целое число.")
self._h = value
Code language: PHP (php)
Давайте проверим, что следующий код выдает ошибку.
a = Time(14, 23, 10)
a.h = "Привет, мир!" # TypeError
Code language: PHP (php)
То же самое произойдет, если аргументы будут неправильного типа, потому что метод __init()__ позаботится о присвоении переданных значений соответствующим атрибутам.
a = Time("Привет, мир!", 23, 10) # TypeError
Code language: PHP (php)
Давайте сделаем то же самое для двух других атрибутов и создадим дополнительный декоратор для проверки того, что аргумент value всегда является целым числом.
from functools import wraps
def _int_required(f):
@wraps(f)
def wrapper(self, value):
if not isinstance(value, int):
raise TypeError("Необходимо ввести целое число.")
return f(self, value)
return wrapper
class Time:
# [...]
@property
def h(self):
return self._h
@h.setter
@_int_required
def h(self, value):
self._h = value
@property
def m(self):
return self._m
@m.setter
@_int_required
def m(self, value):
self._m = value
@property
def s(self):
return self._s
@s.setter
@_int_required
def s(self, value):
self._s = value
Code language: CSS (css)
Отлично! Мы успешно решили проблему типа данных.
Теперь давайте разберемся с другим вопросом.
Мы знаем, что 60 секунд равны одной минуте, а 60 минут равны одному часу.
Необходимо, чтобы наш класс позаботился о балансировке данных автоматически: например, преобразовал 80 секунд в 1 минуту и 20 секунд, присвоив соответствующие атрибуты.
Для этого добавим баланс в методы, вызываемые при присвоении атрибутов m и s (сеттеры).
@m.setter
@_int_required
def m(self, value):
self._m = value
self._h, self._m = _balance(self._h, self._m)
# [...]
@s.setter
@_int_required
def s(self, value):
self._s = value
self._m, self._s = _balance(self._m, self._s)
Code language: PHP (php)
Функция _balance() создается перед определением класса.
def _balance(a, b):
if b >= 0:
while b >= 60:
a += 1
b -= 60
elif b < 0:
while b < 0:
a -= 1
b += 60
return a, b
Code language: JavaScript (javascript)
Я не объясняю, как работает функция, потому что это не главная тема статьи.
Давайте убедимся, что теперь балансировка происходит автоматически.
a = Time(2, 80, 95)
print(a) # <Time 03:21:35>
Code language: PHP (php)
Арифметические операции
Было бы полезно, если бы мы могли складывать и вычитать экземпляры Time с помощью операторов + и -.
Python позволяет реализовать это очень простым способом: определив методы __add()__ и __sub()__ соответственно.
Аналогично __mul()__ и __truediv()__ доступны для умножения (*) и деления (/).
Начнем со сложения.
def __add__(self, other):
h = self.h + other.h
m = self.m + other.m
s = self.s + other.s
return Time(h, m, s)
Code language: PHP (php)
Логика довольно проста: если a и b – два экземпляра Time, выполнение a + b Python вернет результат a.__add(b)__.
a = Time(2, 16, 48)
b = Time(3, 51, 22)
print(a + b) # <Time 06:08:10>
Code language: PHP (php)
Для вычитания процедура аналогична.
def __sub__(self, other):
h = self.h - other.h
m = self.m - other.m
s = self.s - other.s
return Time(h, m, s)
Code language: PHP (php)
Мы также можем выразить это в более функциональной форме следующим образом.
import operator
# [...]
def _operation(self, other, method):
h = method(self.h, other.h)
m = method(self.m, other.m)
s = method(self.s, other.s)
return Time(h, m, s)
def __add__(self, other):
return self._operation(other, operator.add)
def __sub__(self, other):
return self._operation(other, operator.sub)
Code language: PHP (php)
(Стандартный модуль operator определяет функции, которые действуют так же, как операторы Python, например, operator.add(a, b) соответствует а+b).
Теперь, чтобы избежать попыток сложения или вычитания экземпляра Time с объектом любого другого типа, мы должны проверить тип данных аргумента.
Давайте создадим декоратор для этого.
def _time_required(f):
@wraps(f)
def wrapper(self, other):
if not isinstance(other, Time):
raise TypeError("Может работать только с объектом Time.")
return f(self, other)
return wrapper
Code language: PHP (php)
Давайте применим его к двум методам, которые мы только что создали.
@_time_required
def __add__(self, other):
return self._operation(other, operator.add)
@_time_required
def __sub__(self, other):
return self._operation(other, operator.sub)
Code language: PHP (php)
Пример:
a = Time(2, 16, 48)
print(a + 10) # TypeError
Code language: PHP (php)
Идеально! Далее рассмотрим, как сделать возможным сравнение между двумя экземплярами нашего класса.
Сравнение
Магические методы для каждой из операций сравнения следующие.
__lt__()
дляa < b
.__gt__()
дляa > b
.__le__()
дляa <= b
.__ge__()
дляa >= b
.__ne__()
дляa != b
.__eq__()
дляa == b
.
Давайте сначала реализуем последний вариант, который является более простым.
Мы знаем, что один экземпляр Time будет равен другому, когда часы, минуты и секунды совпадают.
@_time_required
def __eq__(self, other):
return (self.h == other.h and
self.m == other.m and
self.s == other.s)
Code language: PHP (php)
Примеры.
print(Time(2, 16, 48) == Time(2, 16, 48)) # True
print(Time(2, 16, 48) == Time(8, 16, 21)) # False
Code language: PHP (php)
Просто! Теперь продолжим с операцией a < b.
В этом случае a будет меньше b, если a.h < b.h.
Если часы равны, нужно проверить минуты. А если они равны, то секунды.
@_time_required
def __lt__(self, other):
if self.h < other.h:
return True
if self.h > other.h:
return False
if self.m < other.m:
return True
if self.m > other.m:
return False
return self.s < other.s
Code language: PHP (php)
Примеры.
print(Time(2, 16, 48) < Time(2, 16, 48)) # False
print(Time(2, 16, 48) < Time(8, 16, 21)) # True
Code language: PHP (php)
Мы могли бы определить остальные методы сравнения так же, как мы это сделали с этими.
Но поскольку другие операции можно вывести из этих двух, которые мы определили (например, a != b равно не a == b), нам не нужно делать это вручную.
Об этом позаботится декоратор functools.total_ordering().
from functools import total_ordering, wraps
# [...]
@total_ordering
class Time:
# [...]
Code language: CSS (css)
Вот и все! Таким образом, мы получили определение всех операторов сравнения.
a = Time(2, 16, 48)
b = Time(8, 16, 21)
print(a == b) # False
print(a != b) # True
print(a > b) # False
print(a < b) # True
print(a >= b) # False
print(a <= b) # True
Code language: PHP (php)
Полный код
Вот мы и добрались до конца! В Python существует множество других магических методов.
Ниже приведен код, который мы разработали на протяжении всей статьи.
#!/usr/bin/env python # -*- coding: utf-8 -*- import operator from functools import total_ordering, wraps def _time_required(f): @wraps(f) def wrapper(self, other): if not isinstance(other, Time): raise TypeError("Может работать только с объектом Time.") return f(self, other) return wrapper def _int_required(f): @wraps(f) def wrapper(self, value): if not isinstance(value, int): raise TypeError("Необходимо ввести целое число.") return f(self, value) return wrapper def _balance(a, b): if b >= 0: while b >= 60: a += 1 b -= 60 elif b < 0: while b < 0: a -= 1 b += 60 return a, b @total_ordering class Time: def __init__(self, h=0, m=0, s=0): self.h = h self.m = m self.s = s @property def h(self): return self._h @h.setter @_int_required def h(self, value): self._h = value @property def m(self): return self._m @m.setter @_int_required def m(self, value): self._m = value self._h, self._m = _balance(self._h, self._m) @property def s(self): return self._s @s.setter @_int_required def s(self, value): self._s = value self._m, self._s = _balance(self._m, self._s) def _operation(self, other, method): h = method(self.h, other.h) m = method(self.m, other.m) s = method(self.s, other.s) return Time(h, m, s) @_time_required def __add__(self, other): return self._operation(other, operator.add) @_time_required def __sub__(self, other): return self._operation(other, operator.sub) @_time_required def __eq__(self, other): return (self.h == other.h and self.m == other.m and self.s == other.s) @_time_required def __lt__(self, other): if self.h < other.h: return True if self.h > other.h: return False if self.m < other.m: return True if self.m > other.m: return False return self.s < other.s def __repr__(self): return f"<Time {self.h:02}:{self.m:02}:{self.s:02}>"