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 ``` **Нескінченні генератори** Генератори чудово підходять коли послідовність не має природного кінця. Оскільки значення ліниві, нескінченний генератор цілком безпечний поки ви контролюєте скільки елементів беретe: ```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)}')