Представьте итератор, который однажды сообщил об окончании обхода. Что обязан возвращать его следующий вызов __next__()?
После первого StopIteration итератор должен оставаться исчерпанным: каждый последующий вызов __next__() обязан снова поднимать StopIteration. Возврат элементов после этого нарушает контракт итератора и может привести к некорректному или бесконечному обходу.
Протокол итераторов отделяет способ получения элементов от структуры коллекции. Он позволил единообразно использовать в for списки, файлы, генераторы и пользовательские объекты, не загружая все данные заранее.
Для этого циклу нужен однозначный сигнал окончания. В Python таким сигналом стало исключение StopIteration, а не специальное значение, которое могло бы совпасть с обычным элементом.
Если итератор после StopIteration внезапно возвращает элемент, потребитель не может надёжно считать его исчерпанным. Цикл for уже завершился, но ручной вызов next() или другой код может снова получить данные.
Особенно опасно нарушение в адаптерах потоков, файловых обёртках и пользовательских итераторах: оно способно вызвать повторную обработку данных, рассинхронизацию состояния или бесконечный цикл.
Метод __next__() возвращает очередной элемент, пока он доступен. Когда элементов больше нет, он поднимает StopIteration; после этого внутреннее состояние итератора должно оставаться таким, чтобы последующие вызовы поднимали то же исключение.
Цикл for перехватывает StopIteration и завершает обход. Само исключение обычно не видно пользователю, но при прямом вызове next() его можно обработать явно.
Минимальный корректный пример:
Здесь первый вызов возвращает элемент, а оба следующих получают запасное значение встроенной функции next, потому что итератор остаётся исчерпанным. Встроенные итераторы Python соблюдают это правило.
Контракт относится именно к итераторам, а не к произвольному объекту, который лишь предоставляет отдельный метод с похожим поведением. При создании собственного итератора состояние окончания нужно фиксировать явно и не пытаться возобновлять выдачу элементов после StopIteration.
Сервис читает страницы API через пользовательский итератор. Один вариант реализации после временной ошибки сбрасывает индекс и начинает выдавать элементы повторно после уже возвращённого StopIteration. Плюс такого подхода только кажущийся: можно повторить чтение, но цена — дублирование записей и нарушение ожиданий потребителей.
Второй вариант создаёт новый итератор для повторной попытки. Это безопаснее: жизненный цикл одного итератора остаётся монотонным, а повторное чтение явно представлено новым объектом. Минус — нужно отдельно управлять повторными попытками и возможным дублированием на уровне бизнес-логики.
Правильным решением будет не оживлять исчерпанный итератор, а создавать новый после успешного решения временной ошибки. Такой дизайн сохраняет контракт протокола и делает поведение for, next() и комбинированных потребителей предсказуемым.
StopIteration возникать только один раз?Нет. Первый StopIteration не означает, что исключение больше не вызывается. Корректный итератор должен поднимать его при каждом последующем вызове __next__().
StopIteration, если это удобно реализации?Нет, не для того же объекта. Возобновление нарушает контракт исчерпанного итератора. Для повторного обхода нужно создать новый итератор либо использовать повторно итерируемый контейнер, который создаёт новый итератор при каждом вызове iter().
Поведение конкретного потребителя может различаться. Цикл for, уже получив StopIteration, завершится и не станет сам повторно вызывать итератор, но ручной код может увидеть неожиданные элементы; конструкции, ожидающие окончательного исчерпания, могут зациклиться или обработать данные повторно. Поэтому такое поведение считается ошибкой реализации, даже если отдельный простой тест проходит.