Вызов reversed для объекта без __reversed__: какой запасной протокол определяет порядок обхода?
Если у типа нет метода __reversed__, Python использует запасной протокол последовательности: вызывает __len__, затем обращается к элементам через __getitem__, начиная с индекса len(obj) - 1 и уменьшая индекс до нуля. Одного метода __iter____ для reversed недостаточно.
Модель данных Python допускает два способа описать обратный обход. Метод __reversed__ позволяет типу явно определить эффективную стратегию, а запасной протокол сохраняет совместимость с объектами, которые ведут себя как индексируемые последовательности, но не объявляют отдельный метод обратного обхода.
Такое разделение отделяет общий интерфейс операции от конкретного способа хранения данных: список может использовать индексы, а специализированная структура — собственный алгоритм.
Неверно считать, что reversed сначала получает обычный итератор через __iter__, а затем разворачивает его. Произвольный итератор обычно нельзя развернуть без предварительного накопления всех элементов, поэтому Python ожидает либо специальный метод, либо доступ к элементам по индексам.
Если объект реализует только __iter__, вызов reversed завершится TypeError. Если объект поддерживает индексирование, но __len__ возвращает некорректный размер или __getitem__ не обрабатывает ожидаемые индексы, обратный обход также не будет корректным.
Сначала Python ищет специальный метод __reversed__ у типа объекта. Если он найден, вызывается именно он, и его результат должен быть итератором. При наличии __reversed__ ошибка из этого метода не приводит автоматически к переходу на запасной протокол.
Если метода нет, Python применяет протокол последовательности. Логика эквивалентна обходу индексов от последнего к первому:
Для такого объекта reversed начинает с индекса 2, затем использует индексы 1 и 0. Реализация __getitem__ должна поддерживать целочисленный доступ; поддержка срезов для самого протокола не требуется.
Запасной вариант не означает, что объект обязан наследоваться от collections.abc.Sequence. Важна фактическая поддержка требуемых специальных методов. При этом наличие только __iter__ не делает объект обратимо итерируемым.
Преимущество __reversed__ — возможность выбрать алгоритм, соответствующий внутреннему представлению данных. Запасной протокол удобен для простых индексируемых объектов, но может быть медленным, если доступ к элементу по индексу дорогой: например, у связного списка переход к каждому индексу может занимать линейное время.
Есть класс, представляющий связный список. Он уже поддерживает __len__, __getitem__ и __iter__, поэтому reversed технически может использовать запасной протокол. Однако обращение к элементу по индексу каждый раз проходит от начала списка, и обратный обход становится квадратичным по времени.
Вариант без __reversed__ прост и не требует дополнительного кода, но плохо масштабируется. Временное преобразование списка в обычный массив делает обратный обход линейным, однако требует дополнительной памяти.
Оптимальное решение — реализовать __reversed__ с учётом структуры связного списка: например, поддержать обратные ссылки или единожды накопить узлы в буфере. Выбор зависит от доступного представления данных, но сам метод должен возвращать итератор и не менять исходный объект. Это делает производительность операции явной частью контракта типа.
Достаточно ли реализовать только __getitem__, чтобы работал reversed?
Нет. Для стандартного запасного протокола требуется также размер объекта через __len__. Без длины Python не знает начальный индекс обратного обхода и обычно сообщает, что объект не поддерживает обратную итерацию.
Будет ли использован запасной протокол, если __reversed__ объявлен, но возвращает неитератор?
Нет. Наличие метода выбирает специальный путь. Если он возвращает неподходящий объект или выбрасывает исключение, Python не обязан пробовать __len__ и __getitem__; ошибка считается ошибкой реализации __reversed__.
Почему объект с __iter__ нельзя автоматически развернуть без дополнительных затрат?
Итератор предоставляет элементы только в направлении продвижения и не обязан знать свою длину, текущую позицию или способ вернуться назад. Поэтому автоматический разворот потребовал бы сначала полностью собрать элементы, что меняет требования к памяти и может быть невозможно для бесконечного итератора. reversed намеренно опирается на обратимый протокол, явно поддержанный самим объектом.