К содержанию

Модуль 6 · Урок 6.3

Итераторы и генераторы

Как for получает элементы, map и filter, генераторные выражения и собственный генератор с yield.

7 задач

Сквозная задача ещё раз: числа пришли строкой "5 -2 10 7". Переведём их в числа через map и посчитаем сумму и наибольшее:

numbers = map(int, "5 -2 10 7".split())print(sum(numbers))print(max(numbers))
20 ValueError: max() iterable argument is empty

Сумма верная, а 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))
10 20 [30] []

iter создаёт итератор, next берёт следующий элемент. Позиция движется только вперёд — как у открытого файла в уроке о файлах:

  1. iter(numbers) создаёт итератор: позиция перед первым элементом.
  2. next выдаёт элемент на позиции и сдвигает её.
  3. Ещё один next — следующий элемент.
  4. list забирает всё, что осталось после позиции.
  5. Позиция уже в конце: итератор исчерпан, второй 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)))
20 10 [5, 10, 7]

list забрал все значения map и сохранил их, а по списку можно ходить сколько угодно. lambda — короткая функция из урока об аргументах.

Частая запись a, b = map(int, input().split()) означает: прочитать строку, разбить её, перевести части в числа и распаковать в два имени. Если частей не две, распаковка завершится ошибкой.

a, b = map(int, input().split())print(a + b)
3 4↵ 7

Списковое включение [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))
56 14

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)))
[1, 4, 9, 16]

list просит у генератора элементы, пока он не закончится: после 16 следующий квадрат 25 больше 20, цикл while завершается, и генератор кончается.

Итог

За каждым for стоит итератор, который выдаёт элементы по одному и не возвращается назад. Вот что понадобится дальше.

  • ИтераторПомнит позицию и выдаёт следующий элемент; iter и next делают вручную то, что делает for.
  • ОдноразовыеФайл, map, filter и генераторы после прохода пусты; список и range обходят снова.
  • map, filterФункция к каждому элементу и отбор подходящих; list(...) собирает результаты, чтобы пройти дважды.
  • Генераторное выражениеsum(x * x for x in numbers) — без промежуточного списка.
  • yieldВыдаёт значение и ставит функцию на паузу до следующего запроса.

Проверьте себя

Ответь сначала сам, потом открой ответ.

Вопрос 1

Как исправить программу из вступления, чтобы она напечатала и сумму, и наибольшее?

ОтветСкрыть ответ
Ответ

Один раз собрать значения в список: numbers = list(map(int, "5 -2 10 7".split())). По списку можно пройти дважды.

Вопрос 2

После it = iter([1, 2, 3]) выполнили next(it). Что вернёт list(it)?

ОтветСкрыть ответ
Ответ

[2, 3]: первый элемент уже забран.

Вопрос 3

Что вернёт list(squares_up_to(10))?

ОтветСкрыть ответ
Ответ

[1, 4, 9]: следующий квадрат 16 больше 10.

Вопрос 4

Что напечатает print(sum(1 for x in range(1, 31) if x % 7 == 0))?

ОтветСкрыть ответ
Ответ

4: кратны 7 числа 7, 14, 21, 28.

Вопрос 5

Почему 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.

Что напечатает программа?

m = map(int, "1 2 3".split())print(sum(m))print(sum(m))

Запишите напечатанные строки через пробел в порядке вывода.

Что напечатает программа?

print(list(filter(lambda x: x % 2, range(10))))

Запишите список так, как его печатает Python.

Какой ошибкой остановится программа?

a, b = map(int, "3 4 5".split())print(a + b)

Введите название исключения.

Что напечатает программа?

def countdown(n):    while n > 0:        yield n        n -= 1 print(list(countdown(3)))

Запишите список так, как его печатает Python.

Напиши программу: первое кратное 17

Напиши одну строку print(next(…)) с генераторным выражением, которая находит первое число от 100 до 199, кратное 17. Запусти её и запиши, что она напечатала.

Введите число.

Напиши генератор: чётные числа

Напиши генератор evens(limit), который выдаёт чётные числа от 2 до limit включительно. Напечатай sum(evens(20)), запусти и запиши, что напечатала программа.

Введите число.