Python — особенный язык в плане итераций и их реализации, в этой статье мы подробно разберём устройство итерируемых объектов и пресловутого цикла for. Как мы видим в этом примере, использование переменной squared_numbers дважды, дало ожидаемый результат в первом случае, и, для людей незнакомых с Python в достаточной мере, неожиданный результат во втором.  Возьмём всё те же переменные:  А теперь, дважды проверим, входит ли элемент в последовательность:  Получившийся результат также может ввести в заблуждение некоторых программистов и привести к ошибкам в коде.  Для примера используем простой словарь с двумя элементами: Распаковываем его: Результат будет также неочевиден, для людей, не понимающих устройство Python, "под капотом": По-сути, вся разница, между последовательностями и итерируемымыи объектами, заключается в том, что в последовательностях элементы упорядочены. Так, последовательностями являются: списки, кортежи и даже строки. Итерируемые объекты же, напротив, не упорядочены, но, тем не менее, могут быть использованы там, где требуется итерация: цикл for, генераторные выражения, списковые включения — как примеры.  Стоит отдельно остановиться на том, что цикл for, в Python, устроен несколько иначе, чем в большинстве других языков. Он больше похож на for...each, или же for...of.  Если же, мы перепишем цикл for с помощью цикла while, используя индексы, то работать такой подход будет только с последовательностями: А с итерируемыми объектами, последовательностями не являющимися, не будет: Если же вам нужен index, то следует использовать встроенную функцию enumerate: Как мы могли убедиться, цикл for не использует индексы. Вместо этого он использует так называемые итераторы. Итераторы — это такие штуки, которые, очевидно, можно итерировать :)
Получить итератор мы можем из любого итерируемого объекта. Для этого нужно передать итерируемый объект во встроенную функцию iter: После того, как мы получили итератор, мы можем передать его встроенной функции next.  При каждом новом вызове, функция отдаёт один элемент. Если же в итераторе элементов больше не осталось, то функция next породит исключение StopIteration. По-сути, это единственное, что мы может сделать с итератором: передать его функции next.
Как только итератор становится пустым и порождается исключение StopIteration, он становится совершенно бесполезным. Используя полученные знания, мы можем написать цикл for, не пользуясь самим циклом for. :) Чтобы сделать это, нам нужно: Стоит заметить, что здесь мы использовали конструкцию try-else. Многие о ней не знают. Она позволяет выполнять код, если исключения не возникло, и код был выполнен успешно. Теперь мы знакомы с протоколом итератора.
А, говоря простым языком — с тем, как работает итерация в Python.
Функции iter и next этот протокол формализуют. Механизм везде один и тот же. Будь то пресловутый цикл for или генераторное выражение. Даже распаковка и "звёздочка" используют протокол итератора: Генераторы тоже реализуют протокол итератора: Подытожим. Итерируемый объект — это что-то, что можно итерировать.
Итератор — это сущность порождаемая функцией iter, с помощью которой происходит итерирование итерируемого объекта. Итератор не имеет индексов и может быть использован только один раз. Теперь формализуем протокол итератора целиком: Особенности: Плюсы итераторов: Итераторы работают "лениво" (en. lazy). А это значит, что они не выполняют какой-либо работы, до тех пор, пока мы их об этом не попросим. Таким образом, мы можем оптимизировать потребление ресурсов ОЗУ и CPU, а так же создавать бесконечные последовательности. Мы уже видели много итераторов в Python.
Я уже упоминал о том, что генераторы — это тоже итераторы.
Многие встроенные функции является итераторами.  Так, например, enumerate: А так же zip: И даже open: В Python очень много итераторов, и, как уже упоминалось выше, они откладывают выполнение работы до того момента, как мы запрашиваем следующий элемент с помощью next. Так называемое, "ленивое" выполнение. Так же, в некоторых случаях, может пригодится знание того, как написать свой собственный итератор и ленивый итерируемый объект.  В моей карьере этот пункт был ключевым, так как вопрос был задан на собеседовании, которое, как вы могли догадаться, я успешно прошёл и получил свою первую работу:)  Так же: Таким образом мы написали бесконечный и ленивый итератор.
А это значит, что ресурсы он будет потреблять только при вызове.
Не говоря уже о том, что без собственного итератора имлементация бесконечной последовательности была бы невозможна. В данном примере, список будет содержать элементы только в первом случае, потому что генераторное выражение — это итератор, а итераторы, как мы уже знаем — сущности одноразовые. И при повторном использовании не будут отдавать никаких элементов. А теперь дважды проверим, входит ли элемент в последовательность:  В данном примере, элемент будет входить в последовательность только 1 раз, по причине того, что проверка на вхождение проверяется путем перебора всех элементов последовательности последовательно, и как только элемент обнаружен, поиск прекращается. Для наглядности приведу пример: Как мы видим, при создании списка из генераторного выражения, в нём оказываются все элементы, после искомого. При повторном же создании, вполне ожидаемо, список оказывается пуст. При использовании в цикле for, словарь будет отдавать ключи: Так как распаковка опирается на тот же протокол итератора, то и в переменных оказываются именно ключи: Последовательности — итерируемые объекты, но не все итерируемые объекты — последовательности.  Итераторы — самая простая форма итерируемых объектов в Python. Любой итерируемый объект реализует протокол итератора. Понимание этого протокола — ключ к пониманию любых итераций в Python. Web Developer