Магические методы: __str__, __repr__, сравнения и контейнеры
В предыдущих статьях мы уже видели, что:
всё в Python — объект
у объекта есть атрибуты и методы, доступные через точку
классы помогают создавать свои объекты, а интроспекция (dir, help, type) — изучать их поведениеТеперь разберём механизм, который связывает обычные операторы и встроенные функции с методами ваших объектов: магические методы (их также называют специальными методами или dunder-методами от double underscore).
Главная идея статьи: когда вы пишете print(obj), obj1 == obj2, x in container или len(container), Python обычно не делает ничего магического — он вызывает конкретные методы вроде __str__, __eq__, __contains__, __len__.
Официальная точка входа в тему: Модель данных Python (Data model).
!Как выражения и встроенные функции превращаются в вызовы магических методов
Что такое магические методы и зачем они нужны
Магический метод — это метод со специальным именем вида __name__, который Python вызывает автоматически в ответ на:
операторы (+, ==, <, in)
встроенные функции (len, repr, str, iter)
протоколы языка (итерация в for, обращение по индексу, форматирование)Это продолжение темы из статьи про атрибуты и методы:
obj.__str__ — обычный атрибут (метод) объекта
отличие в том, что интерпретатор знает, когда его вызыватьПолезная привычка: если вы хотите понять, почему выражение работает так, попробуйте найти соответствующий метод в документации или через dir(obj).
__repr__ и __str__: как объект выглядит для разработчика и для пользователя
В Python есть два основных текстовых представления объекта.
__repr__ — представление для разработчика: точное, диагностическое.
__str__ — представление для пользователя: красивое, читаемое.repr: что делает __repr__
Функция repr(obj) вызывает obj.__repr__() и возвращает строку.
Типичные места, где используется __repr__:
интерактивная консоль Python (когда вы просто вводите имя переменной)
вывод элементов контейнеров (например, список показывает элементы через repr)
логи и отладочная печатьДокументация: repr.
Хорошее практическое правило:
__repr__ должен помогать понять, что это за объект и из чего он состоит
часто делают так, чтобы repr был похож на код создания объекта (когда это разумно)Пример:
str: что делает __str__
Функция str(obj) вызывает obj.__str__().
Документация: str.
print(obj) почти всегда опирается на str(obj).
Документация: print.
Пример, где __str__ делает более дружелюбный вывод:
Здесь важно заметить:
print(p) выводит красивую форму (2, 5)
print([p, p]) выводит что-то вроде [Point(x=2, y=5), Point(x=2, y=5)], потому что контейнеру важнее диагностическая формаЧто будет, если __str__ не определить
Если __str__ не задан, Python обычно использует __repr__ как запасной вариант.
Это удобно:
иногда достаточно определить только __repr__
а __str__ добавлять, когда нужно отдельное пользовательское представлениеf-строки и !r
В f-строках есть полезный суффикс !r, который принудительно использует repr.
Сравнения: __eq__, __lt__ и друзья
Операторы сравнения тоже завязаны на специальные методы.
Равенство: __eq__ и __ne__
a == b пытается вызвать a.__eq__(b)
a != b пытается вызвать a.__ne__(b)Документация по правилам сравнения и специальным методам: Эмуляция числовых типов и сравнения и Основные специальные имена методов.
Практический пример: считаем точки равными, если равны координаты.
Почему важен NotImplemented:
это сигнал интерпретатору: я не умею сравнивать с таким типом
тогда Python может попробовать обратное сравнение (если оно есть) или вернуть FalseДокументация: NotImplemented.
Порядок: __lt__, __le__, __gt__, __ge__
a < b вызывает a.__lt__(b)
a <= b вызывает a.__le__(b)
a > b вызывает a.__gt__(b)
a >= b вызывает a.__ge__(b)Если вы хотите сортировать объекты, чаще всего достаточно определить __lt__ (и, при необходимости, __eq__).
Пример: сортировка по расстоянию до начала координат, но без вычисления корня.
Декоратор @total_ordering помогает не писать все методы порядка вручную: он достроит остальные на основе __eq__ и одного из методов <, <=, > или >=.
Документация: functools.total_ordering.
Важная связка: __eq__ и __hash__
Если объект можно использовать как ключ в dict или элемент в set, он должен быть хешируемым.
по умолчанию пользовательские объекты хешируются по идентичности (как будто по адресу)
но если вы переопределяете __eq__, Python обычно делает __hash__ = None, чтобы объект не стал случайно хешируемым с неправильной логикойПочему так:
ключи словаря и элементы множества должны сохранять хеш во времени
если объект изменяемый и его равенство зависит от изменяемых полей, то после изменения он может оказаться в неправильной корзине структуры данныхДокументация: __hash__.
Практическое правило:
если вы делаете объект сравнимым по значению (__eq__), подумайте, должен ли он быть ключом dict
если должен, обычно объект делают неизменяемым (или, как минимум, не меняют поля, влияющие на равенство и хеш)Контейнеры: len, in, индексация и итерация
Чтобы объект вел себя как контейнер (хотя бы частично), Python использует несколько связанных протоколов.
len(container): __len__
len(obj) вызывает obj.__len__() и ожидает целое число .
Документация: len и __len__.
Пример:
bool(obj): __bool__ и запасной вариант через __len__
Когда Python вычисляет bool(obj) или проверяет объект в if obj:, он действует так:
если есть __bool__, вызывает его
иначе, если есть __len__, то длина 0 считается False, а любая ненулевая — TrueДокументация: __bool__.
item in container: __contains__ и запасные варианты
Оператор in обычно пытается вызвать:
container.__contains__(item)Если __contains__ не определён, Python может перейти к запасным стратегиям:
итерация по контейнеру (если контейнер поддерживает итерацию)
или обращение по индексам, начиная с 0 (для объектов, которые ведут себя как последовательности)Документация: __contains__.
Пример:
Индексация: __getitem__, __setitem__, __delitem__
Скобки тоже завязаны на магические методы.
obj[i] вызывает obj.__getitem__(i)
obj[i] = value вызывает obj.__setitem__(i, value)
del obj[i] вызывает obj.__delitem__(i)Документация: Эмуляция контейнеров.
Пример контейнера-обёртки:
Заметьте связь с темой атрибутов:
self._items — это атрибут-данные, который хранит реальный список
магические методы контейнера делегируют работу этому спискуИтерация: __iter__
Чтобы объект работал в for x in obj, Python пытается получить итератор.
Чаще всего это делается через __iter__.
Документация: __iter__.
Пример:
Как выбирать, какие магические методы писать
Практический ориентир: определяйте магические методы не ради красоты, а ради понятного интерфейса.
Частые минимальные наборы:
удобный вывод: __repr__ и, при необходимости, __str__
равенство по значению: __eq__ (и продумать __hash__)
контейнер: __len__, __iter__ и, если нужно, __contains__ или __getitem__Как исследовать магические методы через интроспекцию
Здесь напрямую применяются инструменты из прошлой статьи.
Примеры того, что полезно делать в практике:
dir(obj) чтобы увидеть, какие методы поддержаны
help(SomeClass.__repr__) чтобы прочитать документацию (особенно для встроенных типов)Итог
Магические методы — это обычные методы объектов, которые Python вызывает автоматически, когда вы используете операторы и встроенные функции.
__repr__ нужен для отладки и разработчика, __str__ — для человеко-читаемого вывода.
Сравнения (__eq__, __lt__ и другие) задают поведение ==, сортировки и порядка; при этом важно помнить про NotImplemented и связь с __hash__.
Контейнерное поведение строится из протоколов: __len__, __contains__, __getitem__, __iter__ и связанных методов.После этой статьи выражения вроде len(x), x in y и print(x) должны читаться как понятные вызовы методов, а не как необъяснимая магия.