Python · Синтаксис · Продвинутый уровень

Генераторы и itertools

10 задач

Функции, возвращающие значения по одному вместо построения полного списка. Охватывает `yield`, `yield from`, генераторные выражения и itertools.

yield, объекты-генераторы, ленивые вычисления и память

#
**Что делает генератор особенным?** Обычная функция вычисляет и возвращает все результаты сразу — вызывающий ждёт пока весь список строится в памяти. Функция-генератор использует `yield` для возврата значений *по одному*, приостанавливая выполнение между ними. Вызывающий получает значение, делает с ним что-то, затем запрашивает следующее: ```python def count_up(n): i = 0 while i < n: yield i # пауза, передаём значение вызывающему i += 1 # возобновление при следующем next() gen = count_up(3) # создаёт объект-генератор, ничего не выполняет print(next(gen)) # 0 print(next(gen)) # 1 print(next(gen)) # 2 print(next(gen)) # StopIteration ``` Ключевая мысль: вызов `count_up(3)` *не* выполняет тело функции. Он создаёт объект-генератор запоминающий где возобновиться. Выполнение начинается только при первом вызове `next()`. **Генератор как машина состояний** Генератор — приостановленная функция. Python сохраняет весь её локальный scope — локальные переменные, указатель инструкции — между вызовами `yield`. Генератор может хранить состояние без класса: ```python def running_total(numbers): total = 0 for n in numbers: total += n yield total # total сохраняется между yield list(running_total([1, 2, 3, 4])) # [1, 3, 6, 10] ``` **Преимущество по памяти** Генератор производит одно значение за раз и сразу отбрасывает его. Использует O(1) памяти независимо от длины последовательности: ```python import sys big_list = [x * x for x in range(1_000_000)] # ~8 МБ big_gen = (x * x for x in range(1_000_000)) # ~200 байт print(sys.getsizeof(big_list)) # 8448728 print(sys.getsizeof(big_gen)) # 208 ``` Генераторные выражения используют `()` вместо `[]`. Работают везде где ожидается итерируемый объект — `sum()`, `max()`, `join()`, циклы `for`: ```python total = sum(x * x for x in range(1_000_000)) # [] не нужны внутри sum() ``` **Когда НЕ стоит использовать генераторы** Генераторы исчерпываются после одного прохода — нельзя итерировать повторно или обращаться по индексу. Если нужно итерировать последовательность несколько раз — материализуйте её: `result = list(my_gen())`. Генераторы не поддерживают `len()`.

yield from, send(), throw(), close() и бесконечные генераторы

#
**`yield from` — делегирование суб-генераторам** `yield from iterable` возвращает каждый элемент из другого итерируемого без ручного цикла. Он также корректно передаёт `send()`, `throw()` и `StopIteration` через цепочку делегирования — то чего ручной цикл `for` сделать не может: ```python def flatten(nested): for item in nested: if isinstance(item, list): yield from flatten(item) # делегируем рекурсивно else: yield item print(list(flatten([1, [2, [3, 4]], 5]))) # [1, 2, 3, 4, 5] ``` **`.send()` — двустороннее общение** Генератор также может *получать* значения. `.send(value)` возобновляет генератор и заставляет `yield` вычислиться как отправленное значение. Первый вызов должен быть `.send(None)` или `next()` для продвижения к первому `yield`: ```python def accumulator(): total = 0 while True: value = yield total # yield отправляет total наружу, получает следующее значение if value is None: break total += value acc = accumulator() next(acc) # продвигаемся к первому yield — возвращает 0 acc.send(10) # total = 10, возвращает 10 acc.send(5) # total = 15, возвращает 15 acc.send(None) # выходит из цикла, StopIteration ``` **`.throw()` и `.close()`** `.throw(exc)` вбрасывает исключение в точке текущего `yield`. `.close()` вбрасывает `GeneratorExit`, давая генератору возможность очиститься в блоке `try/finally`: ```python def managed_gen(): try: while True: yield finally: print('generator cleaned up') # всегда выполняется при .close() g = managed_gen() next(g) g.close() # generator cleaned up ``` **Бесконечные генераторы** Генераторы прекрасно подходят когда последовательность не имеет естественного конца. Поскольку значения ленивые, бесконечный генератор совершенно безопасен пока вы контролируете сколько элементов берёте: ```python def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b import itertools print(list(itertools.islice(fibonacci(), 8))) # [0, 1, 1, 2, 3, 5, 8, 13] ```

itertools, конвейерные паттерны и когда выбрать генератор

#
**itertools — инструментарий генераторов** Модуль `itertools` предоставляет ленивые строительные блоки. Все функции возвращают итераторы, а не списки. Они естественно компонуются для построения конвейеров данных: | Функция | Что делает | Пример | |---|---|---| | `chain(a, b, ...)` | Итерирует последовательности одну за другой | `chain('AB', 'CD')` → A B C D | | `islice(it, n)` | Берёт первые n элементов (ленивая нарезка) | `islice(fib(), 5)` → первые 5 Фибоначчи | | `takewhile(pred, it)` | Выдаёт пока предикат True | `takewhile(lambda x: x<5, count())` | | `dropwhile(pred, it)` | Пропускает пока True, затем выдаёт остальное | `dropwhile(lambda x: x<3, [1,2,3,4])` → 3 4 | | `groupby(it, key)` | Группирует *последовательные* элементы с одинаковым ключом | сначала сортировать | | `product(a, b)` | Декартово произведение | `product('AB', [1,2])` → A1 A2 B1 B2 | | `combinations(it, r)` | Комбинации длины r, без повторений | `combinations('ABC', 2)` → AB AC BC | | `accumulate(it, fn)` | Накопительная редукция | `accumulate([1,2,3,4])` → 1 3 6 10 | **Построение конвейеров** ```python import itertools # Первые 5 чётных квадратов больше 50 squares = (x * x for x in itertools.count(1)) above_50 = itertools.dropwhile(lambda x: x <= 50, squares) even_above_50 = (x for x in above_50 if x % 2 == 0) result = list(itertools.islice(even_above_50, 5)) print(result) # [64, 100, 196, 256, 400] # Группировка слов по первой букве (сначала нужно отсортировать!) words = ['apple', 'avocado', 'banana', 'blueberry', 'cherry'] for letter, group in itertools.groupby(words, key=lambda w: w[0]): print(letter, list(group)) ``` **Когда использовать генератор, а когда список** Генератор — когда итерируете один раз, последовательность большая или бесконечная, строите конвейер где каждый шаг фильтрует или трансформирует. Список — когда нужно итерировать несколько раз, нужен `len()`, индексация или нарезка, или последовательность мала и хотите видеть все значения сразу.
01

Генератор Фибоначчи

#

Напишите функцию-генератор `fibonacci()`, которая бесконечно выдаёт числа Фибоначчи (0, 1, 1, 2, 3, 5, 8, ...). Используйте `itertools.islice`, чтобы получить первые `n` значений.

import itertools

def fibonacci():
    pass


first10 = list(itertools.islice(fibonacci(), 10))
print(first10)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Решение
import itertools

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b


first10 = list(itertools.islice(fibonacci(), 10))
print(first10)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
02

Генератор с дробным шагом

#

Напишите функцию-генератор `frange(start, stop, step)`, которая выдаёт вещественные числа от `start` до `stop` (не включая) с шагом `step`. Она должна работать как `range()`, но поддерживать дробный шаг.

def frange(start, stop, step):
    pass


print(list(frange(0, 1, 0.25)))   # [0, 0.25, 0.5, 0.75]
print(list(frange(1, 2, 0.3)))    # [1, 1.3, 1.6, 1.9]
Решение
def frange(start, stop, step):
    current = start
    while current < stop:
        yield current
        current += step


print(list(frange(0, 1, 0.25)))   # [0, 0.25, 0.5, 0.75]
print(list(frange(1, 2, 0.3)))    # [1, 1.3, 1.6000000000000001, 1.9]
03

Разглаживание вложенных списков через yield from

#

Напишите генератор `flatten(nested)`, который рекурсивно разглаживает вложенный список любой глубины и выдаёт отдельные элементы. Используйте `yield from` для рекурсии. Не-списки выдаются как есть.

def flatten(nested):
    pass


data = [1, [2, 3], [4, [5, 6]], [[7], 8]]
print(list(flatten(data)))  # [1, 2, 3, 4, 5, 6, 7, 8]
Решение
def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item


data = [1, [2, 3], [4, [5, 6]], [[7], 8]]
print(list(flatten(data)))  # [1, 2, 3, 4, 5, 6, 7, 8]
04

Скользящее среднее через send()

#

Напишите генератор-корутину `running_average()`, вычисляющую скользящее среднее. Каждый вызов `.send(value)` должен добавлять `value` к сумме и `yield`-нуть текущее среднее. Запустите генератор через `next()` перед отправкой значений.

def running_average():
    pass


avg = running_average()
next(avg)            # запускаем корутину
print(avg.send(10))  # 10.0
print(avg.send(20))  # 15.0
print(avg.send(30))  # 20.0
Решение
def running_average():
    total = 0
    count = 0
    value = yield  # точка запуска — первый next() останавливается здесь
    while True:
        total += value
        count += 1
        value = yield total / count


avg = running_average()
next(avg)            # продвигаемся до первого yield
print(avg.send(10))  # 10.0
print(avg.send(20))  # 15.0
print(avg.send(30))  # 20.0
05

Генератор скользящего окна

#

Напишите генератор `sliding_window(iterable, n)`, который выдаёт кортежи из `n` последовательных элементов, сдвигаясь на один шаг. Например, `sliding_window([1,2,3,4,5], 3)` выдаёт `(1,2,3)`, `(2,3,4)`, `(3,4,5)`.

from collections import deque

def sliding_window(iterable, n):
    pass


print(list(sliding_window([1, 2, 3, 4, 5], 3)))
# [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
Решение
from collections import deque

def sliding_window(iterable, n):
    window = deque(maxlen=n)
    for item in iterable:
        window.append(item)
        if len(window) == n:
            yield tuple(window)


print(list(sliding_window([1, 2, 3, 4, 5], 3)))
# [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
06

Генераторы take и drop

#

Напишите две функции-генератора: - `take(n, iterable)` — выдаёт первые `n` элементов из iterable. - `drop(n, iterable)` — пропускает первые `n` элементов и выдаёт остальные. Обе должны работать лениво (не конвертировать в список внутри).

def take(n, iterable):
    pass

def drop(n, iterable):
    pass


nums = range(10)
print(list(take(3, nums)))   # [0, 1, 2]
print(list(drop(7, nums)))   # [7, 8, 9]
Решение
def take(n, iterable):
    for i, item in enumerate(iterable):
        if i >= n:
            break
        yield item

def drop(n, iterable):
    for i, item in enumerate(iterable):
        if i >= n:
            yield item


nums = range(10)
print(list(take(3, nums)))   # [0, 1, 2]
print(list(drop(7, nums)))   # [7, 8, 9]

# Или через itertools:
import itertools
list(itertools.islice(nums, 3))       # take(3)
list(itertools.islice(nums, 7, None)) # drop(7)
07

Генератор фрагментов (chunks)

#

Напишите генератор `chunks(iterable, n)`, который разбивает iterable на последовательные фрагменты размером `n`. Последний фрагмент может быть меньше, если длина iterable не делится на `n`. Выдавайте каждый фрагмент как список.

def chunks(iterable, n):
    pass


print(list(chunks(range(10), 3)))
# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Решение
def chunks(iterable, n):
    chunk = []
    for item in iterable:
        chunk.append(item)
        if len(chunk) == n:
            yield chunk
            chunk = []
    if chunk:  # выдаём последний неполный фрагмент
        yield chunk


print(list(chunks(range(10), 3)))
# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

# Python 3.12+: itertools.batched(iterable, n)
08

Генератор простых чисел

#

Напишите генератор `primes()`, который бесконечно выдаёт простые числа с помощью простого пробного деления: для каждого кандидата проверяйте делимость только на ранее найденные простые числа. Используйте `itertools.islice` для первых 10 простых.

import itertools

def primes():
    pass


print(list(itertools.islice(primes(), 10)))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Решение
import itertools

def primes():
    found = []
    candidate = 2
    while True:
        if all(candidate % p != 0 for p in found):
            found.append(candidate)
            yield candidate
        candidate += 1


print(list(itertools.islice(primes(), 10)))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
09

Пайплайн из itertools

#

С помощью `itertools.chain`, `itertools.filterfalse` и генераторного выражения постройте пайплайн, который: 1. Объединяет два списка: `[1, 2, 3]` и `[4, 5, 6]`. 2. Фильтрует чётные числа. 3. Возводит оставшиеся числа в квадрат. Выведите результат как список.

import itertools

a = [1, 2, 3]
b = [4, 5, 6]

# Постройте пайплайн с itertools.chain, itertools.filterfalse
# и генераторным выражением для возведения в квадрат.
result = []
print(result)  # [1, 9, 25]
Решение
import itertools

a = [1, 2, 3]
b = [4, 5, 6]

chained = itertools.chain(a, b)                              # 1 2 3 4 5 6
odds = itertools.filterfalse(lambda x: x % 2 == 0, chained) # 1 3 5
result = list(x * x for x in odds)                          # 1 9 25
print(result)  # [1, 9, 25]
10

Группировка слов по первой букве через itertools.groupby

#

Имея отсортированный список слов, используйте `itertools.groupby`, чтобы сгруппировать их по первой букве и вывести каждую группу на отдельной строке: `'a: apple, avocado'`. Список уже отсортирован по алфавиту.

import itertools

words = ['apple', 'avocado', 'banana', 'blueberry', 'cherry', 'coconut']

# Используйте itertools.groupby для группировки по первой букве
# Ожидаемый вывод:
# a: apple, avocado
# b: banana, blueberry
# c: cherry, coconut
Решение
import itertools

words = ['apple', 'avocado', 'banana', 'blueberry', 'cherry', 'coconut']

for letter, group in itertools.groupby(words, key=lambda w: w[0]):
    print(f'{letter}: {", ".join(group)}')