В классе нет __iter__ и __reversed__, но определены __len__ и индексный доступ. Как reversed() обработает такой объект?
class Batch:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
def __getitem__(self, index):
if not 0 <= index < len(self.items):
raise IndexError
return self.items[index]
batch = Batch(['a', 'b', 'c'])
print(list(reversed(batch)))
reversed(batch) использует протокол последовательности: получает длину через __len__, затем обращается к элементам по индексам от len(batch) - 1 до 0. Поэтому результатом будет ['c', 'b', 'a'], несмотря на отсутствие __reversed__.
В Python поддерживается не только современный протокол итерации с методом __iter__, но и более старый последовательностный протокол. Он позволяет работать с объектами, у которых есть длина и индексный доступ, сохраняя совместимость с пользовательскими последовательностями.
Для обратного обхода этот подход особенно полезен как запасной механизм: если класс не предоставляет специализированный __reversed__, Python может использовать индексы последовательности.
Разработчик может ошибочно считать, что reversed() работает только со списками, кортежами или объектами с __reversed__. Из-за этого он либо добавляет лишний метод, либо не замечает, что обратный обход пользовательского объекта выполняет дорогостоящие индексные операции.
Важно также, чтобы __len__ возвращал корректную длину, а индексный доступ соответствовал этой длине. Иначе обратный обход может завершиться исключением или вернуть неожиданные результаты.
Сначала reversed() проверяет наличие специального метода __reversed__. Если он есть, используется именно он. В приведённом классе такого метода нет, поэтому Python применяет запасной протокол последовательности.
Механизм концептуально эквивалентен следующему обходу:
Фактически объект-реверсивный итератор запоминает последний допустимый индекс и затем уменьшает его. Наличие __iter__ для этого механизма не требуется.
Если индексный доступ имеет сложность O(1), как у списка, полный обратный обход обычно занимает O(n) времени. Если доступ к элементу дорогой, например у структуры с последовательным поиском по индексу, обход может стать существенно медленнее.
Метод __reversed__ нужен, когда обратный порядок нельзя эффективно получить простым обращением к индексам или требуется специальная логика. Если он определён, он имеет приоритет над запасным протоколом.
В API был создан объект ленивой коллекции, который умел получать элемент по индексу, но вычислял его через обращение к удалённому хранилищу. Обычный reversed() формально работал, однако выполнял сетевой запрос для каждого индекса и делал обратный обход медленным.
Рассматривались два варианта. Первый — преобразовывать коллекцию в список перед обходом: это упрощало реализацию, но требовало дополнительной памяти и полностью материализовывало данные. Второй — реализовать __reversed__, который использовал специальный API хранилища для получения элементов в обратном порядке.
Был выбран второй вариант: __reversed__ устранил лишние запросы и сохранил ленивость. Запасной протокол последовательности остался полезным для небольших коллекций, где индексный доступ был дешёвым.
__iter__, но не добавить __reversed__?reversed() всё равно не обязан использовать __iter__. Он сначала ищет __reversed__, а при его отсутствии обращается к последовательностному протоколу через длину и индексы. Поэтому одного __iter__ недостаточно, если класс не поддерживает требуемый индексный доступ.
__reversed__, и __getitem__ с __len__?Будет вызван __reversed__, поскольку это специализированный протокол обратного обхода. Запасной индексный механизм используется только при отсутствии __reversed__. Это позволяет классу явно выбрать более эффективную или семантически корректную реализацию.
__reversed__ может быть важнее, чем кажется?Индексный доступ предполагает, что элементы можно эффективно получать по позициям. Для связанных списков, потоковых источников или удалённых коллекций это может быть неверно: один индексный запрос способен требовать линейного поиска, вычисления или сетевого обращения.
Специализированный __reversed__ может обходить внутреннюю структуру напрямую, использовать буфер или иной API. Цена такого решения — необходимость самостоятельно поддерживать корректность порядка, обработку изменений коллекции и ожидаемую семантику итератора.