Объектно-ориентированное программирование

Специальные методы: __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 проверяет это за тебя.