Модуль 6 · Урок 6.3
Итераторы и генераторы
Как for получает элементы, map и filter, генераторные выражения и собственный генератор с yield.
Сквозная задача ещё раз: числа пришли строкой "5 -2 10 7". Переведём их в числа через map и посчитаем сумму и наибольшее:
numbers = map(int, "5 -2 10 7".split())print(sum(numbers))print(max(numbers))Сумма верная, а max жалуется, что чисел нет. map выдаёт значения по одному и не хранит их: sum забрал все, и второму проходу ничего не осталось. В этом уроке разберём, как for получает элементы, почему одни источники одноразовые, а другие нет, и как написать свой источник значений.
Что выдаёт for
for уже ходил по многим источникам — у всех один механизм. Вспомним, что выдаёт for для каждого знакомого источника:
| Источник | Что получает переменная цикла | Урок |
|---|---|---|
range(a, b, step) | Числа арифметической последовательности | о for |
| Строка | Символы по порядку | о строках |
| Список, кортеж | Элементы по порядку | о списках и кортежах |
| Словарь | Ключи; .items() — пары | о словарях |
| Множество | Элементы без гарантии порядка | о множествах |
enumerate, zip | Пары «номер — элемент», элементы по позициям | о кортежах |
| Открытый файл | Строки файла по одной | о файлах |
Общее у них одно: for просит у источника следующий элемент, пока элементы не кончатся. Кто их выдаёт — разберём дальше.
Итератор
Итератор помнит текущую позицию и по запросу выдаёт следующий элемент. Список, строка, range — перебираемые объекты: каждый из них умеет создать новый итератор. for так и делает: берёт у источника итератор и просит у него элементы. Это можно сделать вручную:
numbers = [10, 20, 30]iterator = iter(numbers)print(next(iterator))print(next(iterator))print(list(iterator))print(list(iterator))iter создаёт итератор, next берёт следующий элемент. Позиция движется только вперёд — как у открытого файла в уроке о файлах:
- iter(numbers) создаёт итератор: позиция перед первым элементом.
- next выдаёт элемент на позиции и сдвигает её.
- Ещё один next — следующий элемент.
- list забирает всё, что осталось после позиции.
- Позиция уже в конце: итератор исчерпан, второй list пуст. Сам список numbers не изменился — из него можно получить новый итератор.
Когда элементы кончились, next вызывает ошибку StopIteration, а for принимает её за обычный конец цикла. Сам список при этом не меняется: новый for по нему создаст новый итератор. Открытый файл и map — сами себе итераторы, поэтому второй проход по ним пуст.
Тонкости:next с запасным значением
next(iterator, None) вместо ошибки вернёт None, когда элементы кончились. Если сам None может быть элементом, такой ответ неотличим от конца.
map и filter
map применяет функцию к каждому элементу, filter оставляет подходящие — оба выдают результаты по одному. map(функция, источник) — результаты функции для каждого элемента. filter(функция, источник) — только те элементы, для которых функция вернула истину. Чтобы пройти по результатам дважды, их один раз собирают в список:
numbers = list(map(int, "5 -2 10 7".split()))print(sum(numbers), max(numbers))print(list(filter(lambda x: x > 0, numbers)))list забрал все значения map и сохранил их, а по списку можно ходить сколько угодно. lambda — короткая функция из урока об аргументах.
Частая запись a, b = map(int, input().split()) означает: прочитать строку, разбить её, перевести части в числа и распаковать в два имени. Если частей не две, распаковка завершится ошибкой.
a, b = map(int, input().split())print(a + b)Списковое включение [int(x) for x in parts] делает то же, что list(map(int, parts)). Выбирай запись, которая понятнее.
Генераторное выражение
Генераторное выражение выдаёт результаты по одному, не создавая список. Оно записывается как списковое включение, только в круглых скобках. Если оно — единственный аргумент функции, хватает её скобок:
numbers = [2, 4, 6]total = sum(x * x for x in numbers)print(total)print(sum(1 for x in range(1, 101) if x % 7 == 0))sum получает квадраты по одному и сразу складывает — список квадратов не создаётся. Вторая строка считает кратные 7: каждое даёт единицу, а sum их складывает.
Это экономит память, когда элементы нужны один раз. Но генератор тоже одноразовый. И быстрее алгоритм от него не становится: миллиард вариантов он переберёт так же долго.
yield
Функция с yield выдаёт значения по одному и между ними стоит на паузе. Такая функция при вызове ничего не считает, а создаёт генератор. Тело начинает выполняться, когда у генератора просят элемент. yield отдаёт значение и ставит функцию на паузу, сохранив её переменные; при следующем запросе выполнение продолжается со строки после yield. return, в отличие от него, завершает вызов окончательно.
Запусти программу по шагам: на каждом yield переменные функции остаются на месте с пометкой «на паузе», а n при продолжении сохраняет прежнее значение.
def squares_up_to(limit): n = 1 while n * n <= limit: yield n * n n += 1 print(list(squares_up_to(20)))list просит у генератора элементы, пока он не закончится: после 16 следующий квадрат 25 больше 20, цикл while завершается, и генератор кончается.
Итог
За каждым for стоит итератор, который выдаёт элементы по одному и не возвращается назад. Вот что понадобится дальше.
- ИтераторПомнит позицию и выдаёт следующий элемент;
iterиnextделают вручную то, что делаетfor. - ОдноразовыеФайл,
map,filterи генераторы после прохода пусты; список иrangeобходят снова. map,filterФункция к каждому элементу и отбор подходящих;list(...)собирает результаты, чтобы пройти дважды.- Генераторное выражение
sum(x * x for x in numbers)— без промежуточного списка. yieldВыдаёт значение и ставит функцию на паузу до следующего запроса.
Проверьте себя
Ответь сначала сам, потом открой ответ.
Как исправить программу из вступления, чтобы она напечатала и сумму, и наибольшее?
ОтветСкрыть ответ
Один раз собрать значения в список: numbers = list(map(int, "5 -2 10 7".split())). По списку можно пройти дважды.
После it = iter([1, 2, 3]) выполнили next(it). Что вернёт list(it)?
ОтветСкрыть ответ
[2, 3]: первый элемент уже забран.
Что вернёт list(squares_up_to(10))?
ОтветСкрыть ответ
[1, 4, 9]: следующий квадрат 16 больше 10.
Что напечатает print(sum(1 for x in range(1, 31) if x % 7 == 0))?
ОтветСкрыть ответ
4: кратны 7 числа 7, 14, 21, 28.
Почему sum(x * x for x in range(1_000_000)) требует меньше памяти, чем sum([x * x for x in range(1_000_000)])? Запись 1_000_000 — то же число 1000000: подчёркивания только облегчают чтение.
ОтветСкрыть ответ
В первом случае каждый квадрат сразу идёт в сумму. Во втором сначала в памяти хранится список из миллиона квадратов. Квадратов вычисляется столько же — экономится память, а не время.
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Что напечатает программа?
it = iter("abc")next(it)next(it)print(list(it))Запишите список так, как его печатает Python.
Подсказка
Два next уже забрали 'a' и 'b'.
Разбор
Итератор строки выдаёт символы по одному. После двух next позиция стоит перед 'c', и list забирает только его.
Ответ: ['c']
Что напечатает программа?
m = map(int, "1 2 3".split())print(sum(m))print(sum(m))Запишите напечатанные строки через пробел в порядке вывода.
Подсказка
map одноразовый: второй sum получает пустой источник.
Разбор
Первый sum забрал 1, 2, 3 и дал 6. Второму ничего не осталось, а сумма пустого — 0.
Ответ: 6 0
Что напечатает программа?
print(list(filter(lambda x: x % 2, range(10))))Запишите список так, как его печатает Python.
Подсказка
x % 2 равно 1 для нечётных — это истина, и 0 для чётных — ложь.
Разбор
filter оставляет числа, для которых функция вернула истину, — нечётные.
Ответ: [1, 3, 5, 7, 9]
Какой ошибкой остановится программа?
a, b = map(int, "3 4 5".split())print(a + b)Введите название исключения.
Подсказка
Значений три, а имён слева два.
Разбор
Распаковка требует ровно столько значений, сколько имён: ValueError: too many values to unpack.
Ответ: ValueError
Что напечатает программа?
def countdown(n): while n > 0: yield n n -= 1 print(list(countdown(3)))Запишите список так, как его печатает Python.
Подсказка
Генератор выдаёт n, потом уменьшает его — пока n > 0.
Разбор
Выдаются 3, 2, 1; при n = 0 цикл while кончается, и генератор тоже.
Ответ: [3, 2, 1]
Напиши программу: первое кратное 17
Напиши одну строку print(next(…)) с генераторным выражением, которая находит первое число от 100 до 199, кратное 17. Запусти её и запиши, что она напечатала.
Введите число.
Подсказка
next берёт первый элемент генераторного выражения x for x in range(100, 200) if ….
Разбор
Например:
print(next(x for x in range(100, 200) if x % 17 == 0))17 · 6 = 102 — первое кратное 17 не меньше 100.
Ответ: 102
Напиши генератор: чётные числа
Напиши генератор evens(limit), который выдаёт чётные числа от 2 до limit включительно. Напечатай sum(evens(20)), запусти и запиши, что напечатала программа.
Введите число.
Подсказка
Цикл с yield n и шагом 2; сумма 2 + 4 + … + 20.
Разбор
Например:
def evens(limit): n = 2 while n <= limit: yield n n += 2 print(sum(evens(20)))2 + 4 + … + 20 = 110.
Ответ: 110