Специальные методы: __repr__, __str__, __eq__, __len__
Содержание курса
eq и len: равенство по значению и поддержка len()
Если __repr__ и __str__ управляют тем, как объект выглядит, то __eq__ и __len__ управляют тем, как он себя ведёт — в сравнениях и при подсчёте размера.
Что происходит без eq
По умолчанию оператор == в Python сравнивает объекты по идентичности — то есть фактически проверяет, что это один и тот же объект в памяти, как is. Два объекта с абсолютно одинаковыми данными будут считаться неравными:
class Playlist:
def __init__(self, name, tracks):
self.name = name
self.tracks = tracks
p1 = Playlist("Rock", ["Song A", "Song B"])
p2 = Playlist("Rock", ["Song A", "Song B"])
print(p1 == p2) # False — разные объекты в памяти
print(p1 is p2) # False
Это типичная ловушка при написании тестов: ты создаёшь два объекта с одинаковыми полями, сравниваешь их через ==, получаешь False — и начинаешь искать баг там, где его нет.
Как работает eq
__eq__(self, other) вызывается каждый раз, когда Python видит ==. Твоя задача — определить, что именно означает «равенство» для твоего типа. Обычно это сравнение значимых атрибутов:
def __eq__(self, other):
if not isinstance(other, Playlist):
return NotImplemented
return self.name == other.name and self.tracks == other.tracks
Здесь важна строчка с NotImplemented. Если other — не Playlist, мы не возвращаем False, а возвращаем специальный синглтон NotImplemented. Это сигнал Python: «я не знаю, как сравнивать с этим типом — попробуй спросить у другой стороны». Python после этого вызовет other.__eq__(self), и если тот тоже вернёт NotImplemented — вот тогда уже будет False. Возврат False напрямую отрезает эту возможность и может сломать корректное сравнение с типом, который умеет сравниваться с Playlist.
len: размер объекта и булев контекст
__len__(self) возвращает неотрицательное целое число. Python вызывает его при len(obj). Для плейлиста естественный размер — количество треков:
def __len__(self):
return len(self.tracks)
Но у __len__ есть побочный эффект, о котором часто забывают: Python использует его при проверке истинности объекта в булевом контексте. Если __len__ возвращает 0, объект считается ложным:
empty = Playlist("Empty", [])
full = Playlist("Rock", ["Song A"])
if empty:
print("есть треки") # не выполнится
if full:
print("есть треки") # выполнится
bool(empty) # False
bool(full) # True
Это поведение работает автоматически — никакого __bool__ определять не нужно, пока есть __len__. Если хочешь переопределить булеву логику отдельно от размера, тогда уже нужен __bool__, но это отдельная история.
Одно жёсткое ограничение: __len__ обязан возвращать целое неотрицательное число. Попытка вернуть отрицательное значение или не-целое поднимет TypeError или ValueError на уровне интерпретатора — Python проверяет это за тебя.
