📦 Репозиторий занятия 54

Урок 54. Итераторы и генераторные выражения

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
PythonИсполняемый пример71 строк

Iterable и Iterator: в чём разница

less_28__iterators__itertools/theory_01_iterable_vs_iterator.py

Пошагово доказывает разницу между итерируемым объектом и итератором на примере списка: список — Iterable, но не Iterator, а результат его __iter__() — уже оба сразу. Показывает, что next() работает только на итераторе, что итератор исчерпывается (StopIteration) и что оператор in для итератора срабатывает только один раз, потому что сдвигает внутренний курсор. В конце сравнивается размер в памяти списка и итератора через sys.getsizeof — итератор не растёт вместе со списком.

  • isinstance(lst, Iterable) / Iterator — список только Iterable
  • lst.__iter__() — получение итератора, теперь оба isinstance True
  • next(lst) — TypeError: 'list' object is not an iterator
  • next(lst_iterator) x7 и восьмой — StopIteration
  • 3 in lst_iterator — True затем False (курсор сдвинулся)
  • size(lst) vs size(lst_iterator) — 120 против 48, и это не меняется при lst*10

Что выводит: True, False; затем True, True для итератора; TypeError на next(list); числа 1..7 по одному, затем StopIteration; True затем False для `3 in`; размеры 120 и 48, потом 616 и 48.

Начало файла
"""iterable vs iterator

ИТЕРИРУЕМЫЙ ОБЪЕКТ - это объект, который может состоять из множества элементов.

ИТЕРАТОР - это объект, который позволяет поочерёдно получать элементы итерируемого объекта
без необходимости загружать их все в память сразу.

(Сам итератор - тоже является итерируемым объектом).
"""

from typing import Iterator, Iterable
from sys import getsizeof as size

lst = [1, 2, 3, 4, 5, 6, 7]
Показать файл целиком (71 строк)
"""iterable vs iterator

ИТЕРИРУЕМЫЙ ОБЪЕКТ - это объект, который может состоять из множества элементов.

ИТЕРАТОР - это объект, который позволяет поочерёдно получать элементы итерируемого объекта
без необходимости загружать их все в память сразу.

(Сам итератор - тоже является итерируемым объектом).
"""

from typing import Iterator, Iterable
from sys import getsizeof as size

lst = [1, 2, 3, 4, 5, 6, 7]

print(isinstance(lst, Iterable))  # True
print(isinstance(lst, Iterator))  # False

print(""" ================= 1. К итерируемому объекту можно применить iter() или __iter__() ================= """)

lst_iterator = lst.__iter__()

print(isinstance(lst_iterator, Iterable))  # True
print(isinstance(lst_iterator, Iterator))  # True

print(""" ================= 2. Но ТОЛЬКО к итератору можно применить next() или __next__() ================= """)

try:
    next(lst)
except Exception as e:
    print(f"{e.__class__.__name__}: {e}")  # TypeError: 'list' object is not an iterator


print(next(lst_iterator))
print(next(lst_iterator))
print(next(lst_iterator))
print(next(lst_iterator))
print(next(lst_iterator))
print(next(lst_iterator))
print(next(lst_iterator))


try:
    print(next(lst_iterator))
except StopIteration as e:
    print(f"{e.__class__.__name__}: {e}")  # StopIteration:

"""ВЫВОД: итератор может исчерпать все свои значения"""

print(""" ================= 3. Отсюда: операция in работает для итератора ТОЛЬКО ОДИН РАЗ!!! ================= """)

lst_iterator = lst.__iter__()
print('3 in lst_iterator', 3 in lst_iterator)  # True
print('3 in lst_iterator', 3 in lst_iterator)  # False


print(""" ================= 4. Что требует бОльшей памяти: iterator или iterable? ================= """)

print(size(lst))   # 120
print(size(lst_iterator))  # 48

# Теперь увеличим lst в 10 раз
lst_10 = lst * 10
lst_10_iterator = iter(lst_10)

print(size(lst_10))   # 616
print(size(lst_10_iterator))  # 48
"""ВЫВОД: По сути, iterator можно считать "ленивой" версией iterable-объекта:
элементы там не хранятся в полном объёме, а "выдаются" только в момент обращения
к итератору с помощью next() или __next___()
"""
Проверьте себя: Почему size(lst_iterator) не меняется, даже когда сам список вырастает в 10 раз?
Открыть файл →
2
PythonИсполняемый пример16 строк

Цикл for изнутри: что на самом деле делает Python

less_28__iterators__itertools/theory_02_loop_for_another_point_of_view.py

Показывает, что цикл for — это синтаксический сахар над связкой iter() + next() + обработка StopIteration. Первый вариант — обычный for, второй — его разворачивание вручную через while True с try/except StopIteration и break.

  • Вариант 1: for item in my_list — привычный цикл
  • Вариант 2: lst_iterator = iter(my_list)
  • while True + next(lst_iterator) внутри try
  • except StopIteration: break — ручная остановка

Что выводит: Дважды печатает 1, 2, 3 — сначала обычным for, потом ручной эмуляцией через iter/next/StopIteration.

Файл целиком (16 строк)
"""Новый взгляд на цикл for"""

my_list = [1, 2, 3]

# ------- var 1 ----------
for item in my_list:
    print(item)


# ------- var 2 ----------
lst_iterator = iter(my_list)
while True:
    try:
        print(next(lst_iterator))
    except StopIteration:
        break
Проверьте себя: Какую часть работы цикла for берёт на себя интерпретатор, а какую в этом файле пришлось написать руками?
Открыть файл →
3
PythonИсполняемый пример24 строк

Изменение списка во время итерации — ловушка «пилишь сук, на котором сидишь»

less_28__iterators__itertools/theory_03__Iterator_immutability.py

Демонстрирует классическую ошибку: внутри for i, value in enumerate(lst) на каждой итерации вызывается lst.append(i). Поскольку список растёт быстрее, чем итератор его проходит, цикл не завершается сам — это реальный бесконечный цикл, а не образное выражение. Второй блок показывает решение: итерировать по копии lst[:] (или lst.copy()), при этом дописывать в оригинальный lst безопасно.

  • for i, value in enumerate(lst): lst.append(i) — растущий список
  • Комментарий: «пилить сук, на котором сидишь»
  • Решение: enumerate(lst[:]) — итерация по копии
  • lst.append(i) внутри цикла по копии — безопасно

Что выводит: Первый цикл — бесконечный: список растёт быстрее, чем итератор его обходит, скрипт не завершается сам (пришлось прерывать вручную). Если закомментировать первый блок и оставить только цикл по lst[:], он печатает конечное число пар (i, value) и корректно завершается.

Начало файла
"""
Итератор сам по себе является НЕИЗМЕНЯЕМЫМ.

НО!
Никто не запрещает нам изменять объект, на который он ссылается.
"""

lst = [1, 2, 3]

for i, value in enumerate(lst):
    print(i, value)
    lst.append(i)

Показать файл целиком (24 строк)
"""
Итератор сам по себе является НЕИЗМЕНЯЕМЫМ.

НО!
Никто не запрещает нам изменять объект, на который он ссылается.
"""

lst = [1, 2, 3]

for i, value in enumerate(lst):
    print(i, value)
    lst.append(i)


"""
Изменять объект по которому итерируешь == пилить сук, на котором сидишь)

Решение проблемы:
"""


for i, value in enumerate(lst[:]):  # или lst.copy()
    print(i, value)
    lst.append(i)
Проверьте себя: Почему итерация по lst[:] решает проблему, а итерация по самому lst — нет, хотя append вызывается в обоих случаях?
Открыть файл →
4
MarkdownРазбор концепции27 строк

Справочник по модулю itertools

less_28__iterators__itertools/theory_04__itertools.md

Таблица-шпаргалка по функциям itertools: count, cycle, repeat, accumulate, chain, compress, dropwhile, takewhile, filterfalse, islice, starmap, product, permutations, combinations, combinations_with_replacement, groupby, zip_longest, а также pairwise (3.10+) и batched (3.12+). Для каждой функции — назначение, пример вызова и результат.

  • Бесконечные генераторы: count, cycle, repeat
  • Накопление и объединение: accumulate, chain, compress
  • Отбор по условию: dropwhile, takewhile, filterfalse, islice
  • Комбинаторика: product, permutations, combinations(_with_replacement)
  • Группировка и выравнивание: groupby, zip_longest
  • Новое в 3.10/3.12: pairwise, batched
Показать начало файла (27 строк всего)
```python
import itertools
```

## Методы пакета `itertools`

| Метод                                                  | Назначение                                                                            | Пример использования                       | Результат                         |
| ------------------------------------------------------ |---------------------------------------------------------------------------------------| ------------------------------------------ | --------------------------------- |
| `itertools.count(start=0, step=1)`                     | Бесконечный счётчик (генератор последовательности чисел).                             | `count(10, 2)`                             | 10, 12, 14, 16, ...               |
| `itertools.cycle(iterable)`                            | Зацикливает элементы итерируемого объекта.                                            | `cycle('AB')`                              | A, B, A, B, A, ...                |
| `itertools.repeat(elem, n=None)`                       | Повторяет элемент `elem` бесконечно или `n` раз.                                      | `repeat('X', 3)`                           | X, X, X                           |
| `itertools.accumulate(iterable, func=operator.add)`    | Накопление результатов (по умолчанию — сумма).                                        | `accumulate([1, 2, 3, 4])`                 | 1, 3, 6, 10                       |
| `itertools.chain(*iterables)`                          | Соединяет несколько итерируемых объектов.                                             | `chain('ABC', 'DEF')`                      | A, B, C, D, E, F                  |
| `itertools.compress(data, selectors)`                  | Отбирает элементы, где `selectors` истинны.                                           | `compress('ABCDEF', [1, 0, 1, 0, 1, 1])`   | A, C, E, F                        |
| `itertools.dropwhile(pred, iterable)`                  | Пропускает элементы, пока предикат возвращает `True`, <br>потом выдаёт все остальные. | `dropwhile(lambda x: x < 5, [1,4,6,7])`    | 6, 7                              |
| `itertools.takewhile(pred, iterable)`                  | Берёт элементы, пока предикат возвращает `True`.                                      | `takewhile(lambda x: x < 5, [1,4,6,7])`    | 1, 4                              |
| `itertools.filterfalse(pred, iterable)`                | Оставляет только элементы, где предикат ложен.                                        | `filterfalse(lambda x: x%2, range(5))`     | 0, 2, 4                           |
| `itertools.islice(iterable, start, stop, step)`        | Срез итератора (как срез списка, но для любого итератора).                            | `islice(range(10), 2, 8, 2)`               | 2, 4, 6                           |
| `itertools.starmap(func, iterable)`                    | Как `map`, но принимает аргументы в виде тюплов.                                      | `starmap(pow, [(2,5), (3,2)])`             | 32, 9                             |
| `itertools.product(*iterables, repeat=1)`              | Декартово произведение элементов.                                                     | `product('AB', '12')`                      | (A,1), (A,2), (B,1), (B,2)        |
| `itertools.permutations(iterable, r=None)`             | Все перестановки длины `r` (по умолчанию полной длины).                               | `permutations('ABC', 2)`                   | AB, AC, BA, BC, CA, CB            |
| `itertools.combinations(iterable, r)`                  | Все комбинации без повторений длины `r`.                                              | `combinations('ABC', 2)`                   | AB, AC, BC                        |
| `itertools.combinations_with_replacement(iterable, r)` | Комбинации с повторениями.                                                            | `combinations_with_replacement('ABC', 2)`  | AA, AB, AC, BB, BC, CC            |
| `itertools.groupby(iterable, key=None)`                | Группировка последовательных элементов по ключу.                                      | `groupby('AAAABBBCCDAA')`                  | (A, [A,A,A,A]), (B, [B,B,B]), ... |
| `itertools.zip_longest(*iterables, fillvalue=None)`    | Аналог `zip`, но выравнивает по самому длинному итератору.                            | `zip_longest('AB', '1234', fillvalue='-')` | (A,1), (B,2), (-,3), (-,4)        |
| `itertools.pairwise(iterable)` *(Python 3.10+)*        | Возвращает пары соседних элементов.                                                   | `pairwise([1,2,3,4])`                      | (1,2), (2,3), (3,4)               |
| `itertools.batched(iterable, n)` *(Python 3.12+)*      | Разбивает итератор на группы по `n` элементов.                                        | `batched('ABCDEFG', 3)`                    | (A,B,C), (D,E,F), (G,)            |
Проверьте себя: Чем combinations отличается от permutations и в каком случае результат product('AB', '12') будет таким же по длине, как у permutations?
5
PythonИсполняемый пример45 строк

Генераторное выражение — частный случай итератора

less_28__iterators__itertools/theory_05__generator_expression.py

Показывает, что генераторное выражение (n for n in range(3)) — это готовый итератор: у него сразу можно вызывать next(), а после исчерпания он бросает StopIteration. Вторая часть — таблица isinstance-проверок для трёх сущностей (список, iter(списка), генераторное выражение) против Iterable, Iterator, Generator, показывающая, что список — только Iterable, iter(list) — Iterable+Iterator, а генератор — все три сразу.

  • gen = (n for n in range(3)) — создание генераторного выражения
  • next(gen) x3, затем StopIteration
  • isinstance(lst, ...) — Iterable=True, Iterator=False, Generator=False
  • isinstance(lst_iter, ...) — Iterable=True, Iterator=True, Generator=False
  • isinstance(gen, ...) — все три True
  • Вопрос в конце файла: можно ли генераторное выражение использовать в for?

Что выводит: Печатает объект <generator object <genexpr> ...>, затем 0, 1, 2, затем 'StopIteration None'; далее блок isinstance: True/False/False, затем True/True/False, затем True/True/True.

Начало файла
"""
Генераторное выражение - частный случай итератора
"""

from typing import Iterable, Iterator, Generator

gen = (n for n in range(3))

print(gen)  # <generator object <genexpr> at 0x74bdc034da80>
print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 2
try:
    print(next(gen))
Показать файл целиком (45 строк)
"""
Генераторное выражение - частный случай итератора
"""

from typing import Iterable, Iterator, Generator

gen = (n for n in range(3))

print(gen)  # <generator object <genexpr> at 0x74bdc034da80>
print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 2
try:
    print(next(gen))
except StopIteration as e:
    print(e.__class__.__name__, e.value)
    # StopIteration None


print(""" ===== Who is who? ===== """)

lst = [1, 2, 3, 4, 5]
lst_iter = iter(lst)
gen = (n for n in range(3))

print(isinstance(lst, Iterable))
print(isinstance(lst, Iterator))
print(isinstance(lst, Generator))
print()

print(isinstance(lst_iter, Iterable))
print(isinstance(lst_iter, Iterator))
print(isinstance(lst_iter, Generator))
print()

print(isinstance(gen, Iterable))
print(isinstance(gen, Iterator))
print(isinstance(gen, Generator))


"""
==============================================================================
Вопрос "на засыпку": Можно ли использовать генераторное выражение в цикле for?
==============================================================================
"""
Проверьте себя: Что напечатает isinstance для gen, и почему только генератор проходит все три проверки — Iterable, Iterator и Generator?
Открыть файл →