Специальные методы Python: repr, len и контейнерный протокол
Специальные методы связывают пользовательский класс с синтаксисом и встроенными функциями Python. Выражение len(obj) находит __len__ на типе объекта — концептуально это похоже на…
Специальные методы связывают пользовательский класс с синтаксисом и встроенными функциями Python. Выражение len(obj) находит __len__ на типе объекта — концептуально это похоже на type(obj).__len__(obj); специальный lookup обходит одноимённый атрибут экземпляра. Оператор in может использовать __contains__, индексирование — __getitem__, а repr(obj) — __repr__. Вызывающий код получает знакомый протокол, но класс обязан соблюдать его смысловые ограничения. Не стоит вызывать такие методы напрямую без причины: обычная форма len(obj) яснее и оставляет интерпретатору проверку результата.
Представление объекта для разработчика
__repr__ должен вернуть строку, полезную при отладке. Хорошее представление показывает тип объекта и значимые поля, не раскрывая секреты. Оно не обязано быть красивой фразой для конечного пользователя. Для такой фразы существует __str__; если __str__ отсутствует, str использует __repr__ как запасной вариант.
Размер и контейнерные операции
__len__ возвращает неотрицательное целое число. Встроенная функция bool тоже учитывает длину: объект с нулевой длиной считается ложным, если класс не определяет отдельный __bool__. Метод __contains__ реализует проверку item in obj. __getitem__ отвечает за доступ по индексу или ключу и может также сделать объект итерируемым по последовательным целочисленным индексам, хотя явный __iter__ обычно лучше выражает намерение.
Рабочий пример контейнера
class ReadingList:
def __init__(self, titles: list[str]) -> None:
self._titles = list(titles)
def __repr__(self) -> str:
return f'ReadingList({self._titles!r})'
def __len__(self) -> int:
return len(self._titles)
def __contains__(self, title: object) -> bool:
return title in self._titles
def __getitem__(self, index: int) -> str:
return self._titles[index]
books = ReadingList(['Python Tricks', 'Fluent Python'])
print(repr(books))
print(len(books))
print('Fluent Python' in books)
print(books[0])Ожидаемый результат
ReadingList(['Python Tricks', 'Fluent Python'])
2
True
Python TricksКопирование входного списка в конструкторе защищает контейнер от неожиданного изменения через исходную переменную. При этом пример остаётся изменяемым изнутри класса: протокол сам по себе не обещает неизменяемость.
Как диагностировать нарушенный контракт протокола
Если __len__ возвращает строку или отрицательное число, len(books) не примет результат. Например, return '2' приводит к TypeError, а отрицательное значение — к ValueError. Начинайте диагностику с контракта вызываемой встроенной функции, затем запустите type(books.__len__()) только как временную проверку.
Другая частая ошибка — вернуть из __repr__ не строку. Сообщение TypeError: __repr__ returned non-string буквально указывает нарушенный контракт. Если индекс выходит за диапазон, естественно передать наружу IndexError внутреннего списка: вызывающий код ожидает именно такое поведение последовательности.
Практикум: контейнер Playlist
Реализуйте Playlist, которая хранит названия треков. Добавьте __repr__, __len__, поиск через in и индексирование. Затем проверьте пустой плейлист: len должен вернуть 0, а bool(playlist) — False. Самостоятельная проверка: убедитесь, что последний допустимый индекс читается, следующий вызывает IndexError, а repr содержит имя класса и данные, позволяющие отличить два экземпляра.
Контракты специальных методов
Специальный метод — реализация общего протокола, а не произвольный метод с необычным именем. Возвращаемый тип и семантика важнее краткости кода. __repr__ помогает разработчику, __len__ сообщает размер, __contains__ отвечает на проверку принадлежности, а __getitem__ задаёт индексный доступ.
Вопросы о repr, len и итерации
Следует ли писать books.__len__() напрямую?
Обычно нет: используйте len(books). Встроенная функция узнаваема и проверяет, что реализация вернула допустимое целое значение.
Обязан ли repr создавать выражение, которое можно выполнить?
Это полезная цель для простых объектов, но не абсолютное требование. Главное — однозначное и безопасное диагностическое представление.
Достаточно ли __getitem__ для цикла for?
Python умеет последовательно запрашивать индексы до IndexError, однако __iter__ обычно точнее, эффективнее и явно документирует поддержку итерации.