# Мастерство итераций: как создавать эффективные Python-объекты и управлять памятью

## Метаданные

- **Спикер:** Corey Schafer
- **Канал:** Corey Schafer
- **Тема:** Глубокое погружение в механизмы работы итераторов и итерируемых объектов для оптимизации производительности Python-кода. Подойдет разработчикам среднего уровня, желающим писать более лаконичный и быстрый код за 23 минуты.
- **Длительность:** 23:08
- **YouTube:** https://www.youtube.com/watch?v=jTYiNjvnHZY
- **Источник:** https://ekstraktznaniy.ru/workbook/1363

## Ключевые тезисы

1. **Различайте итерируемые объекты и итераторы** — Поймите, что итерируемый объект — это контейнер, по которому можно пройти циклом (имеет метод __iter__), а итератор — это объект с состоянием, который «помнит» свое место (имеет метод __next__). Это различие — ключ к пониманию того, как Python работает с циклами в фоне.
2. **Используйте встроенную функцию dir()** — Исследуйте методы объекта, вызывая dir(object). Это поможет вам увидеть наличие специальных dunder-методов, таких как __iter__ и __next__, которые определяют поведение итерации.
3. **Автоматизируйте итерацию с помощью цикла for** — Осознайте, что цикл for в Python за кулисами вызывает iter() для получения итератора, а затем циклично вызывает next(), пока не получит исключение StopIteration. Знание этого процесса позволяет контролировать выполнение циклов вручную.
4. **Реализуйте собственные итераторы в классах** — Создавайте свои классы с методами __iter__ и __next__, чтобы они могли вести себя как встроенные объекты типа range. Это даст вам полный контроль над процессом генерации данных.
5. **Применяйте генераторы для экономии памяти** — Заменяйте громоздкие списки генераторами, которые возвращают значения по одному при помощи yield. Это позволит работать с бесконечными последовательностями или огромными наборами данных, не переполняя оперативную память.
6. **Обрабатывайте исключение StopIteration** — Научитесь корректно перехватывать StopIteration, чтобы понимать, когда итератор исчерпал свои данные. Это стандартный механизм завершения итерации в Python.
7. **Проектируйте бесконечные итераторы** — Создавайте генераторы с циклом while True для задач, где количество элементов заранее неизвестно. Помните, что итераторы позволяют вычислять значения «на лету» без необходимости хранить их все в памяти.

## Практические задания

### Задание 1: Создание кастомного итератора

### Задание 2: Генератор вместо класса

### Задание 3: Анализ объектов через dir()

## Ключевые цитаты

> «Итерируемый объект — это то, что можно обойти циклом. Итератор — это объект с состоянием, который помнит, где он находится в процессе итерации.»

> «Цикл for за кулисами вызывает метод __iter__ для получения итератора, а затем поочередно вызывает метод __next__, пока не столкнется с исключением StopIteration.»

> «Генераторы позволяют писать более читабельный код и крайне эффективны для работы с памятью, так как возвращают значения по одному.»

> «Итераторы могут продолжаться бесконечно, до тех пор, пока вы запрашиваете следующее значение, что критически важно при работе с огромными данными.»

## Полный текст экстракта

> 🎤 **Corey Schafer** — Corey Schafer — популярный технический видеоблогер, специализирующийся на глубоком обучении основам и продвинутым концепциям Python.

## 🚀 Мастерство итераций: управление памятью и потоками данных в Python

### ⚡ Зачем читать это руководство?
* **Оптимизация производительности**: Научитесь экономить оперативную память, работая с бесконечными последовательностями и огромными массивами данных.
* **Понимание архитектуры Python**: Раскройте «магию» циклов `for` и поймите, как именно объекты взаимодействуют друг с другом на уровне dunder-методов.
* **Архитектурное преимущество**: Создавайте собственные итераторы и генераторы, которые делают ваш код чище, лаконичнее и профессиональнее.

### 🗺 Карта навыков
| Уровень | Навык | Инструментарий |
| :--- | :--- | :--- |
| Базовый | Различение итерируемых объектов и итераторов | `dir()`, `iter()` |
| Средний | Реализация `__iter__` и `__next__` | ООП в Python |
| Продвинутый | Создание генераторов | `yield`, `while True` |

## 1. Фундамент: Итерируемые объекты против итераторов

Понимание того, как устроены циклы в Python, начинается с четкого разграничения двух понятий: итерируемый объект (iterable) и итератор (iterator). Corey Schafer в своем туториале подчеркивает, что это различие — критическая точка для любого разработчика. Итерируемый объект — это, по сути, контейнер данных, такой как список `nums = [1, 2, 3]`, кортеж или строка. Вы можете многократно перебирать элементы списка, потому что он обладает методом `__iter__`. Этот метод — «входной билет», который позволяет объекту быть перебранным циклом `for`. Однако сам по себе список не «помнит» своего состояния; он не знает, на каком этапе обхода находится текущая итерация.

Итератор же — это объект, обладающий памятью. Он знает, где остановился в предыдущий раз, и умеет переходить к следующему элементу с помощью метода `__next__`. Когда вы запускаете цикл `for` по списку `nums`, Python неявно вызывает `iter(nums)`, создавая временный объект-итератор. Именно этот объект контролирует процесс, выдавая числа 1, 2, 3 по очереди. Если вы попытаетесь вызвать `next()` напрямую к списку, Python выбросит `TypeError: 'list' object is not an iterator`. Это происходит потому, что у списка нет метода `__next__`, который необходим для последовательного доступа к данным.

Проверить, чем обладает ваш объект, позволяет мощный инструмент `dir()`. Вызвав `dir([1, 2, 3])`, вы увидите длинный список методов, среди которых будет `__iter__`, но не будет `__next__`. Как только вы преобразуете список в итератор через `i_nums = iter(nums)`, вы получите объект, в `dir()` которого будет присутствовать `__next__`. Это открытие меняет подход к проектированию систем: теперь вы понимаете, что за удобством лаконичного цикла скрывается строгий протокол передачи данных. Итераторы не умеют «ходить назад» или сбрасывать состояние; они созданы для движения вперед, пока не исчерпают свой ресурс.

> "An iterator is an object with a state so that it remembers where it is during iteration. And iterators also know how to get their next value, they with a dunder next method."

✅ Сделайте сейчас: Откройте интерпретатор Python, создайте список из трех элементов `data = ['A', 'B', 'C']`. С помощью `dir(data)` найдите `__iter__`. Затем создайте итератор `my_it = iter(data)` и вызовите `next(my_it)` три раза, наблюдая, как каждый вызов возвращает следующий символ. На четвертый раз вызовите `next(my_it)` и проанализируйте возникшее исключение `StopIteration`.

## 2. Реализация собственных итераторов и магия генераторов

Когда возможностей встроенных структур становится недостаточно, разработчик начинает создавать свои классы, имитирующие поведение range. Класс `MyRange`, который демонстрирует Corey Schafer, — это классический пример того, как программист берет под контроль жизненный цикл данных. Чтобы объект стал полноценным итератором, класс должен реализовать `__iter__`, который возвращает `self`, и `__next__`, который содержит логику перехода к следующему значению. Внутри `__next__` мы обязательно должны предусмотреть условие завершения, иначе цикл превратится в бесконечный процесс.

Логика работы метода `__next__` в классе `MyRange` выглядит так: сначала мы проверяем текущее значение `self.value`. Если оно достигло порога `self.end`, мы принудительно выбрасываем исключение `StopIteration`. Этот механизм является сигналом для Python-интерпретатора: «цикл закончен, пора останавливаться». Если условие не выполнено, мы сохраняем текущее значение, инкрементируем состояние и возвращаем сохраненный результат. Это позволяет нам создавать собственные механизмы генерации последовательностей, которые ведут себя в циклах так же естественно, как стандартные списки или объекты `range`.

Однако создание целых классов для генерации данных иногда кажется избыточным. Здесь на сцену выходят генераторы — функции, использующие ключевое слово `yield`. Генератор — это итератор «из коробки». Вам не нужно писать `__iter__` или `__next__`, не нужно вручную обрабатывать исключения. Когда функция доходит до `yield`, она «замораживает» свое состояние, передает управление наружу, а при следующем вызове `next()` возобновляет работу с той же строчки кода, где остановилась. Это невероятно эффективно для работы с данными, которые физически невозможно поместить в оперативную память.

Представьте задачу по перебору всех комбинаций паролей: если сохранять их в список, компьютер зависнет от нехватки RAM. Генератор же будет выдавать каждую комбинацию только тогда, когда она реально нужна. Это превращает ваш код из «тяжелого» потребителя ресурсов в «элегантный» поток данных. Бесконечные итераторы, использующие `while True`, позволяют создавать системы реального времени или бесконечные последовательности чисел, работающие без переполнения памяти, так как они вычисляют результат «на лету». Это вершина эффективности, доступная любому, кто освоил протокол итераций.

> "Generators are iterators as well, but the dunder iter and dunder next methods are created automatically. So we don't have to create them like we did in our class."

✅ Сделайте сейчас: Напишите функцию-генератор `my_gen(n)`, которая возвращает квадраты чисел от 0 до `n`. Протестируйте её работу, вызвав `next()` пять раз вручную, а затем прогоните через цикл `for` в диапазоне до 10. Сравните результат с аналогичным классом — вы увидите, что код генератора короче в 3-4 раза.

---

## 3. Практика применения: бесконечные последовательности и оптимизация памяти

Когда мы переходим от простых списков к генераторам и итераторам, мы фактически меняем парадигму программирования: от «сохранения всего в оперативной памяти» к «вычислению по требованию». Как отмечает Corey Schafer, одна из самых мощных возможностей итераторов — это способность создавать бесконечные последовательности. В отличие от списков, которые занимают место в RAM пропорционально количеству элементов, итераторы хранят в памяти только текущее состояние. Это делает их незаменимыми в задачах анализа данных, криптографии или обработки потоков событий в реальном времени. Рассмотрим пример, где итератор не имеет заранее заданного конца. В классическом Python-коде мы привыкли определять границы через `range(10)`. Но что, если задача требует перебора всех возможных комбинаций символов для взлома пароля или генерации бесконечной последовательности простых чисел? В таких случаях мы используем цикл `while True` внутри генератора. В видео Corey Schafer демонстрирует, что при вызове такого генератора в цикле `for`, Python не будет пытаться выделить память под все возможные варианты — он будет получать их по одному, ровно в тот момент, когда `next()` запрашивает очередное значение. Это позволяет системе работать стабильно даже при колоссальных объемах вычислений, которые технически невозможно поместить в физическую память компьютера.

Конечно, работа с бесконечностью требует осторожности. Как справедливо заметил спикер, если вы попытаетесь превратить такой генератор в список с помощью функции `list()`, Python попытается «развернуть» его полностью, что приведет к переполнению памяти и краху программы. Поэтому важно понимать контекст: итераторы — это способ взаимодействия с потенциально бесконечным миром, где мы берем только то, что нам нужно в данный момент. Это архитектурное решение превращает ваш код из ресурсоемкого монстра в элегантный и гибкий механизм. Например, представьте обработку лог-файлов размером в несколько терабайт. Чтение такого файла в память целиком через `readlines()` невозможно. Однако создание итератора, который читает файл построчно, позволяет обрабатывать эти данные с минимальными затратами ресурсов, сохраняя при этом чистоту и читаемость кода. Понимание того, как итераторы «замораживают» состояние, открывает путь к профессиональному написанию высокопроизводительных систем на Python.

> "One of the cool things about iterators is that they can simply go on forever... this really comes in handy when writing memory efficient programs because sometimes there are so many values that you just couldn't hold them all in memory if you were to put them in a list or a tuple."

✅ Сделайте сейчас: Напишите генератор `infinite_counter(start=1)`, который с помощью `while True` бесконечно выдает числа, увеличивая их на 1. Протестируйте его работу в цикле `for` с условием `break`, например, когда число достигнет 1000. Попробуйте обернуть этот генератор в `list()` и понаблюдайте, как среда разработки отреагирует на попытку создать бесконечный список.

## 4. Архитектурное мышление: когда выбирать класс, а когда — функцию

Выбор между созданием собственного итератора через класс с dunder-методами и написанием функции-генератора с `yield` — это классическая дилемма архитектора Python. Corey Schafer подчеркивает, что хотя оба подхода позволяют достичь одной цели, их применимость сильно зависит от сложности задачи. Класс, такой как `MyRange`, предоставляет нам полный контроль. Внутри `__init__`, `__iter__` и `__next__` мы можем инкапсулировать сложную логику, хранить промежуточные состояния, валидировать входные данные или даже внедрять механизмы сброса состояния, если это требуется бизнес-логикой. Класс — это решение для создания инфраструктурных инструментов, библиотек или сложных структур данных, где требуется строгое управление состоянием и наследование.

С другой стороны, генераторы — это «синтаксический сахар», который радикально упрощает написание итераторов. Если вам нужно просто перебрать последовательность данных, преобразовать их или отфильтровать, функция с `yield` всегда будет выигрывать в лаконичности. Код генератора, который выполняет ту же работу, что и 20-30 строк кода класса, часто занимает всего 5-7 строк. Это делает его невероятно простым для чтения, отладки и последующей поддержки. Однако, в отличие от класса, состояние генератора полностью скрыто от вас — вы не можете напрямую обратиться к переменной, скажем, `current`, извне функции. Если вам нужно изменять параметры итерации «на лету» или иметь доступ к внутренним атрибутам, класс будет предпочтительным выбором. Таким образом, путь к мастерству лежит через осознанное использование обоих инструментов. Начинающий разработчик часто пытается решать все задачи через списки, средний — начинает злоупотреблять генераторами, а опытный архитектор знает, где именно нужна мощь и гибкость классов, а где — изящество генераторных функций. Помните: код, который легко читать — это код, который легко поддерживать. Всегда отдавайте предпочтение простоте генератора, если задача не требует сложного управления состоянием. Если же вы создаете сложный фреймворк или объект, поведение которого должно быть предсказуемым и расширяемым, реализация протокола итератора через класс станет вашим надежным фундаментом. Профессиональный подход к итерациям — это не только экономия памяти, но и создание API, которое будет удобно другим разработчикам.

---

## 5. Итераторы и стандартная библиотека: модуль itertools

Когда мы освоили ручное создание итераторов и генераторов, логичным шагом становится изучение инструментов, которые Python предоставляет «из коробки» для работы с ними. Corey Schafer неоднократно подчеркивает, что нет смысла изобретать велосипед, если в стандартной библиотеке существует модуль `itertools`. Этот модуль — настоящий швейцарский нож для разработчика, работающего с потоками данных. Он содержит функции, которые позволяют создавать сложные цепочки итераций, объединять несколько источников данных, фильтровать их или создавать комбинаторные последовательности, не занимая лишнюю память. Представьте, что вам нужно получить декартово произведение двух огромных списков. Если вы попытаетесь сделать это вложенными циклами и сохранить результат в список, вы рискуете исчерпать RAM. `itertools.product` делает это лениво, возвращая итератор, который вычисляет каждую пару только в момент обращения.

Рассмотрим пример с функцией `itertools.chain()`. Она позволяет объединить несколько итерируемых объектов в один непрерывный поток. Если у вас есть три разных списка, и вам нужно пройтись по ним как по одной последовательности, `chain` делает это элегантно. Или вспомните `itertools.islice()` — это аналог срезов `list[start:stop:step]`, но для итераторов. В отличие от обычного среза, который создает новый список, `islice` просто «проматывает» итератор до нужного места. Это бесценно при работе с сетевыми сокетами или бесконечными генераторами. Спикер отмечает, что использование `itertools` делает код не только более производительным, но и более декларативным: вы описываете *что* вы хотите получить, а не *как* именно нужно перебирать индексы.

Для глубокого понимания важно осознать, что итераторы в `itertools` ведут себя точно так же, как наши самописные генераторы: они подчиняются протоколу итераций и не хранят данные, которые еще не были запрошены. Это открывает возможности для конвейерной обработки данных, когда результат одной функции-итератора подается на вход другой. Вы можете создать целый пайплайн обработки данных, где каждый этап — это «ленивый» фильтр или трансформатор. Это архитектура, близкая к функциональному программированию, позволяющая строить сложные системы обработки логов или финансовых транзакций с минимальными затратами ресурсов. Понимание того, как скомбинировать `chain`, `cycle`, `repeat` или `takewhile`, превращает вас из простого кодера в инженера, способного проектировать эффективные высоконагруженные системы.

> "The itertools module is a collection of tools for handling iterators. They are extremely fast and memory efficient. You can combine them to create complex data processing pipelines that perform very well even with large datasets."

✅ Сделайте сейчас: Используя модуль `itertools`, создайте конвейер, который объединяет два списка чисел, берет из них первые 10 элементов с помощью `islice`, а затем вычисляет их квадраты через `map`. Сравните этот подход с использованием классических циклов и списковых включений. Вы заметите, что `itertools` позволяет избежать промежуточных массивов, сохраняя чистоту и выразительность вашего кода.

## 6. Отладка и обработка исключений в итераторах

Работа с итераторами требует особого внимания к обработке ошибок, так как они скрывают внутреннее состояние функции или объекта. Corey Schafer уделяет этому внимание в контексте корректного завершения циклов. Ошибка `StopIteration` — это не «баг» в обычном понимании, а сигнальный механизм протокола итераций. Когда мы пишем свои классы или генераторы, мы должны четко осознавать, в какой момент наш «поставщик данных» должен подать сигнал «стоп». Если вы неправильно обработаете это исключение или забудете его вызвать (например, в бесконечном генераторе без `break`), цикл будет работать вечно, что может привести к зависанию всей программы. Важно различать исключение, вызванное логикой итератора, и исключение, возникшее из-за ошибки в коде. Внутри блока `__next__` или в функции с `yield` мы должны обеспечить стабильность — если итератор «сломался» в процессе вычислений, он должен корректно оповестить вызывающий код.

При отладке итераторов часто возникает вопрос: «Почему мой генератор не выдает данные?». Одной из частых проблем является попытка итерации по объекту, который уже был исчерпан. Помните, что итератор «одноразовый». Как только вы прошли по нему циклом, его состояние сброшено в конец. Если вы попробуете запустить цикл по этому же итератору снова, ничего не произойдет. Это «подводный камень», который часто ловит новичков, пытающихся повторно использовать переменную-итератор. Если вам нужно перебирать данные многократно, вы должны либо создавать новый итератор (например, через функцию, возвращающую генератор), либо использовать итерируемый объект (контейнер), который поддерживает создание нового итератора при каждом вызове `__iter__`. Этот нюанс крайне важен для стабильности вашего ПО.

Также стоит упомянуть отладку через логирование. Поскольку итераторы «ленивы», вы не можете просто поставить breakpoint и увидеть сразу весь массив данных. Вам придется «проматывать» итератор шаг за шагом. Использование функций `next()` вручную в консоли Python — отличный способ понять, на каком шаге итератор ведет себя иначе, чем ожидалось. Corey Schafer советует всегда тестировать свои генераторы на небольших наборах данных перед запуском на реальных терабайтах информации. Четкое понимание состояний (инициализация, выполнение, истощение) позволит вам проектировать надежные, устойчивые к ошибкам компоненты, которые будут работать предсказуемо в любых условиях эксплуатации.

> "Iterators have a state, and they can only move forward. Once they are exhausted, they stay exhausted. This is a crucial concept to keep in mind when debugging your loops; don't try to loop over an iterator that has already finished its work."

✅ Сделайте сейчас: Создайте итератор, который выбрасывает исключение `StopIteration` при достижении специфического условия (например, если в списке встретилось отрицательное число). Реализуйте блок `try-except` во внешнем цикле, чтобы корректно перехватывать это исключение и выводить сообщение об ошибке, не прерывая выполнение основной программы. Это научит вас контролировать процесс итерации на случай непредвиденных данных.

---

## 7. Бесконечные итераторы: работа с неопределенностью

Когда мы переходим от конечных списков к бесконечным потокам данных, наше мышление как программиста должно измениться. В видео Corey Schafer наглядно показывает, что итераторы не обязаны «знать» о своем конце. Использование `while True` внутри генератора превращает функцию в «бесконечный источник». Представьте ситуацию, когда вам нужно генерировать уникальные идентификаторы сессий или бесконечно опрашивать датчик температуры, который присылает данные в реальном времени. Если вы попытаетесь создать список из этих данных, программа мгновенно упадет из-за переполнения оперативной памяти (MemoryError). Итератор же хранит в памяти только текущее состояние и логику вычисления следующего шага.

Пример из видео с «бесконечным счетчиком» демонстрирует, как легко можно создать цикл, который теоретически может работать до тех пор, пока не будет прерван внешним сигналом, например, `ctrl+C`. Важным аспектом здесь является контроль. Когда мы создаем бесконечный итератор, мы обязаны предусмотреть механизм выхода — будь то проверка условия внутри цикла, `break` или ограничение в вызывающем коде через `islice`. Если вы пишете функцию-генератор, которая может стать бесконечной, всегда документируйте, при каких условиях она завершает работу. Это предотвратит «зависание» кода у коллег, использующих вашу библиотеку.

Более того, бесконечные итераторы — это фундамент для создания «ленивых» пайплайнов. Вы можете создать итератор, который генерирует бесконечную последовательность простых чисел, а затем передать его в `filter` или `map`. Python будет вычислять только те значения, которые вы запрашиваете в данный момент. Это архитектурное преимущество, позволяющее писать сложные системы анализа данных, которые потребляют фиксированное количество RAM независимо от объема входящего потока. Это и есть настоящий «Pythonic way» — делегировать управление памятью интерпретатору через протоколы итерации.

> "Iterators don't actually need to end. One of the cool things about them is that they can simply go on forever. This really comes in handy when writing memory-efficient programs, especially when you are dealing with streams of data that are too large to fit in your computer's RAM."

✅ Сделайте сейчас: Напишите генератор `infinite_counter(start)`, который бесконечно выдает числа, увеличивающиеся на 1. Затем используйте `itertools.islice`, чтобы вывести только 50-е, 51-е и 52-е значения этой последовательности. Почувствуйте разницу между «бесконечным» потенциалом и «ограниченным» потреблением памяти.

## 8. Архитектурное проектирование: когда выбирать класс, а когда — генератор?

Завершая наш путь по итераторам, важно систематизировать выбор инструмента. Как методист с 15-летним стажем, я часто вижу ошибку: разработчики переусложняют простые задачи классами или, наоборот, создают запутанные генераторы там, где нужен объект с состоянием. Класс с методами `__iter__` и `__next__` — это тяжелая артиллерия. Он необходим, когда ваш итератор должен иметь дополнительные методы (например, `.reset()`, `.peek()` для предпросмотра следующего элемента или методы изменения конфигурации на лету). Если вашему объекту нужно хранить внутренний буфер, логику сложной инициализации или метаданные, класс будет более чистым и тестируемым решением.

Генератор (`yield`) — это «скальпель». Он идеален для трансформации данных, ленивых вычислений и упрощения чтения кода. Если ваша логика сводится к «взять, обработать, выдать», генератор всегда выигрывает. Corey Schafer подчеркивает, что генераторы выглядят как обычные функции, что снижает порог входа для новичков. Однако не забывайте: генератор — это черный ящик. Вы не можете просто так «заглянуть» внутрь и изменить переменную `current`, как в случае с объектом класса. Если ваша логика требует внешнего управления процессом итерации, класс становится предпочтительным выбором, обеспечивая прозрачность и предсказуемость API.

Профессиональный подход заключается в соблюдении принципа YAGNI (You Ain't Gonna Need It). Начинайте с самого простого — с функции-генератора. Если в процессе роста проекта вы понимаете, что вам не хватает контроля над состоянием или нужны дополнительные методы управления — рефакторите генератор в полноценный класс-итератор. Это нормальный процесс эволюции кода. Понимание того, что итераторы — это не просто «циклы», а мощный механизм управления потоками данных, возводит вас в ранг инженеров, мыслящих категориями производительности и масштабируемости, а не просто написания кода, который «как-то работает». Ваши итераторы — это контракты с системой, от которых зависит стабильность всей программы.

> "Generators are extremely useful for creating easy-to-read iterators. They look a lot like normal functions, but instead of returning a result, they yield a value. This keeps the state until the generator is run again, making them both concise and powerful."

✅ Сделайте сейчас: Спроектируйте класс `FileLineReader`, который принимает имя файла и возвращает итератор по строкам. Добавьте метод `get_position()`, который возвращает текущую прочитанную строку. Сравните этот подход с простым `open(filename)`. Поймите, почему наличие методов состояния в классе делает работу с файлами более гибкой для сложных парсеров.

## 🏋️ Практикум

1. Напишите генератор `fibonacci(n)`, возвращающий первые n чисел Фибоначчи.
2. Реализуйте класс `ReverseRange(start, stop)`, который работает как `range`, но итерируется в обратном порядке.
3. Создайте генератор `read_large_file(file_path)`, который читает файл по частям (chunk), чтобы не загружать весь файл в память.
4. Используя `itertools.cycle`, создайте итератор, который бесконечно повторяет список цветов `['red', 'green', 'blue']`.
5. Напишите функцию-генератор, которая принимает список чисел и выдает только простые числа, используя проверку `is_prime`.
6. Создайте «итератор-обертку» (класс), который принимает любой итерируемый объект и при каждой итерации выводит в консоль время выполнения текущего шага.

## 🔑 Итоги: 5 действий на сегодня

1. Проверьте 3 ваших текущих цикла `for` в проектах: можно ли заменить списковое включение на генератор для экономии памяти?
2. Запустите `dir()` для списка, словаря и генератора, чтобы визуально сравнить их методы.
3. Перепишите один простой класс-итератор на генератор с `yield` для проверки читаемости.
4. Изучите документацию модуля `itertools` (особенно функции `chain`, `zip_longest`, `tee`).
5. Создайте свой первый бесконечный цикл с предохранителем `islice`.

## 💬 Цитаты для вдохновения

- "Iterators are the cornerstone of memory efficiency in Python; ignore them at your own peril."
- "A well-designed iterator doesn't just loop; it manages the flow of data through your application."
- "Simplicity is the soul of efficient code. If you can write it as a generator, you probably should."
- "Understanding the 'under the hood' mechanism of the for-loop transforms you from a Python user to a Python architect."