Python · Синтаксис · Просунутий рівень
Генератори та itertools
Функції, що повертають значення по одному замість побудови повного списку. Покриває `yield`, `yield from`, вирази-генератори та itertools.
Короткий вступ до теми та пояснення перед вправами (вправи нижче):
yield from, send(), throw(), close() та нескінченні генератори
#itertools, конвеєрні патерни та коли обрати генератор
#Вправи:
Генератор Фібоначчі
#Напишіть функцію-генератор `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]
Генератор з дробовим кроком
#Напишіть функцію-генератор `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]
Розгладжування вкладених списків через 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]
Ковзне середнє через 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
Генератор ковзного вікна
#Напишіть генератор `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)]
Генератори 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)
Генератор фрагментів (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)
Генератор простих чисел
#Напишіть генератор `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]
Пайплайн з 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]
Групування слів за першою літерою через 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)}')