Специальные методы 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__ обычно точнее, эффективнее и явно документирует поддержку итерации.

Источники