Язык программирования Python

Классы в Python: магические методы и свойства

Каждый разработчик 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}>"

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

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