**Що робить генератор особливим?**
Звичайна функція обчислює і повертає всі результати одразу — викликач чекає поки весь список будується в пам'яті. Функція-генератор використовує `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()`, індексація або нарізка, або послідовність мала і хочете бачити всі значення одразу.
Напишіть функцію-генератор `fibonacci()`, яка нескінченно видає числа Фібоначчі (0, 1, 1, 2, 3, 5, 8, ...). Використовуйте `itertools.islice`, щоб отримати перші `n` значень.
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()`, але підтримувати дробовий крок.
Напишіть генератор `flatten(nested)`, який рекурсивно розгладжує вкладений список будь-якої глибини та видає окремі елементи. Використовуйте `yield from` для рекурсії. Не-списки видаються як є.
Напишіть генератор-корутину `running_average()`, що обчислює ковзне середнє. Кожен виклик `.send(value)` повинен додавати `value` до суми та `yield`-нути поточне середнє. Запустіть генератор через `next()` перед відправкою значень.
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)`.
Напишіть дві функції-генератори:
- `take(n, iterable)` — видає перші `n` елементів з iterable.
- `drop(n, iterable)` — пропускає перші `n` елементів і видає решту.
Обидві мають працювати ліниво (не конвертувати в список внутрішньо).
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(iterable, n)`, який розбиває iterable на послідовні фрагменти розміром `n`. Останній фрагмент може бути меншим, якщо довжина iterable не ділиться на `n`. Видавайте кожен фрагмент як список.
Напишіть генератор `primes()`, який нескінченно видає прості числа за допомогою простого пробного ділення: для кожного кандидата перевіряйте ділення лише на раніше знайдені прості числа. Використовуйте `itertools.islice` для перших 10 простих.
За допомогою `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)}')
No split tab
Налаштування cookies
Ми використовуємо необхідні cookies для роботи сайту. З вашого дозволу ми також можемо зберігати налаштування сайту та використовувати аналітичні й рекламні cookies, щоб розуміти використання сайту й підтримувати розвиток проєкту.
* Ви завжди можете змінити свій вибір у налаштуваннях сайту.
Оберіть категорії cookies
Налаштування аналітики
Можна вимкнути аналітику використання платформи. Також можна надіслати в Google Analytics запит на видалення даних про використання цього сайту, пов'язаних із цим браузером.
Навчальний workspace
Навчайтеся, читаючи, запускаючи код і розв'язуючи задачі.
Практикуйте програмування з поясненнями тем, вправами, інструментами browser IDE, перевіркою regex і тренуванням друку коду в одному workspace.
Відкривайте інструменти у вкладках.Вправи, IDE-інструменти й тренажери залишаються доступними як вкладки сайту.
Перемикайтеся без втрати контексту.Переходьте між поясненнями, кодом та утилітами, зберігаючи своє місце.
Використовуйте sidebar як карту.Ліві панелі містять навігацію, налаштування, файли, libraries та керування інструментами.
PythonJavaScriptSQLite
Одна IDE, три практичні режими
Python у браузері.Запускайте невеликі скрипти, пробуйте бібліотеки й тренуйте API-запити без встановлення.
JavaScript для швидких експериментів.Перевіряйте код для браузера й порівнюйте ідеї поруч із навчальними матеріалами.
SQLite для практики з даними.Відкривайте оглядач бази даних, переглядайте таблиці, пишіть запити й вивчайте SQL локально.
ТемаIDE
Працюйте поруч у split tabs
Тримайте інструкції перед очима.Відкрийте вправу або довідкову сторінку поруч з IDE, замість постійних перемикань.
Порівнюйте інструменти під час навчання.Розміщуйте перевірки regex, пояснення та експерименти з кодом поруч, коли це потрібно для задачі.
Закрийте split, коли завершите.Workspace повернеться до однієї сфокусованої вкладки, а відкриті вкладки сайту залишаться доступними.
Тренажер друку коду
Або звичайного тексту
Тренажер розрахований на фізичну клавіатуру.Відкрийте цей розділ на ноутбуці або комп'ютері з широким екраном. На телефоні тренування друку не працюватиме коректно.
Швидкість: 0 зн/хв
0 слів/хв
Найкраща швидкість (60с): 0 зн/хв
0 слів/хв
Помилки: 0
Загальний час: 0.0 с
Щоб тренувати сліпий друк, не підглядайте на фізичну клавіатуру.