Урок 25. Ітератори advanced¶
Застосунок кур'єрів «Смачно + Таксі» шле в диспетчерську потік подій: кур'єр забрав замовлення (picked), кур'єр доставив (delivered). Кожна подія — рядок «час кур'єр замовлення подія». Так виглядає журнал вечірньої зміни:
LOG = [
"17:52 D-2 98 delivered",
"17:58 D-1 97 delivered",
"18:03 D-1 101 picked",
"18:05 D-2 102 picked",
"18:07 D-3 103 picked",
"18:21 D-1 101 delivered",
"18:24 ?? зламаний рядок",
"18:29 D-2 102 delivered",
"18:31 D-1 104 picked",
"18:44 D-3 103 delivered",
"18:52 D-1 104 delivered",
]
print(len(LOG))
Тут усе, з чим стикається справжній потік: події попередньої зміни (до 18:00), битий рядок, доставки різних кур'єрів упереміш. А диспетчерській потрібно:
- середній час доставки на льоту, після кожної події, а не наприкінці дня;
- звіт по кур'єрах;
- лише події поточної зміни;
- список битих рядків, щоб розібратися з ними, а не губити мовчки.
В уроці 11 ми навчилися будувати конвеєри з генераторів. Сьогодні — наступний рівень: власні ітератори-класи, генератори, які приймають дані через .send(), генератор як скінченний автомат і «алгебра» itertools.
Що потрібно з попередніх уроків: ітератори, yield, конвеєри й islice (урок 11), декоратори (урок 10), скінченний автомат статусів (урок 22), __iter__ (урок 24), перебір з поверненням (урок 23).
Після уроку ти зможеш:
- писати клас-ітератор з
__iter__і__next__і відокремлювати ітерабельне від ітератора; - пояснювати стани генератора,
returnу генераторі,close()іfinally; - передавати дані в генератор через
.send()і запускати його декоратором; - будувати скінченний автомат на генераторі;
- використовувати
yield fromдля делегування й отримання результату; - обирати інструмент
itertools:dropwhile,takewhile,groupby,pairwise,accumulate,combinations; - проєктувати ETL-конвеєр зі стадіями, які легко тестувати, і окремим списком відхилених записів.
Задача розділу. Конвеєр «журнал → розбір → зміна → тривалості → звіт», що рахує середній час доставки кожного кур'єра й збирає биті рядки окремо. Повний код — у розділі «Практика».
Ноутбук заняття: Відкрити вправи в Colab Переглянути розв’язки
Пригадай¶
- Що робить
forз об'єктом, перш ніж узяти перший елемент? - Чому другий
list()від того самого генератора порожній? - Як в уроці 22 автомат статусів вирішував, чи дозволений перехід?
- Що повертав
__iter__кошика в уроці 24?
Відповіді
- Викликає
iter()і отримує ітератор, а далі —next(), доки не прийдеStopIteration. - Генератор одноразовий: перший
list()дійшов до кінця. - Шукав пару «поточний статус → новий» у словнику
TRANSITIONS. - Готовий ітератор словника:
iter(self._items.items()).
Клас-ітератор: __iter__ і __next__¶
В уроці 11 ми бачили протокол ззовні: iter(), потім next(), доки не StopIteration. Щоб наш клас сам був ітератором, треба два методи: __next__ віддає наступний елемент або кидає StopIteration, а __iter__ повертає сам ітератор. Перша спроба — журнал зміни, що перебирає власні рядки:
class ShiftLog:
def __init__(self, lines):
self.lines = lines
self.position = 0
def __iter__(self):
return self
def __next__(self):
if self.position >= len(self.lines):
raise StopIteration
line = self.lines[self.position]
self.position += 1
return line
log = ShiftLog(LOG[2:5])
print(list(log))
print(list(log))
Працює — один раз. Курсор position живе в самому журналі, тож після першого проходу журнал «порожній», хоча рядки нікуди не ділися. Так само зламаються вкладені цикли по тому самому журналу: внутрішній цикл вичерпає курсор для зовнішнього.
Проблема в тому, що ми змішали дві ролі:
- ітерабельне (журнал) — знає дані й на кожен
iter()видає новий ітератор; - ітератор (курсор) — пам'ятає позицію, одноразовий.
flowchart LR
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
LOG["ShiftLog<br>дані"] -- "iter()" --> C1["курсор 1<br>position = 0"]
LOG -- "iter()" --> C2["курсор 2<br>position = 0"]
C1 -- "next()" --> L["рядок"]
C1 -- "next() в кінці" --> STOP["StopIteration"]
class LOG success
class C1,C2 step
class L step
class STOP warning
Розділяємо ролі на два класи:
class ShiftLogIterator:
def __init__(self, lines):
self._lines = lines
self._position = 0
def __iter__(self):
return self
def __next__(self):
if self._position >= len(self._lines):
raise StopIteration
line = self._lines[self._position]
self._position += 1
return line
class ShiftLog:
def __init__(self, lines):
self.lines = list(lines)
def __iter__(self):
return ShiftLogIterator(self.lines)
log = ShiftLog(LOG[2:5])
print(len(list(log)), len(list(log)))
print(len([(a, b) for a in log for b in log]))
Кожен for отримує свій курсор: два проходи по 3 рядки, вкладений цикл — 3 × 3 пар. А __iter__ ітератора повертає self для того, щоб ітератор теж можна було передати в for.
Писати окремий клас-курсор доводиться рідко. Якщо __iter__ — генератор, Python сам створює новий курсор на кожен виклик:
class ShiftLog:
def __init__(self, lines):
self.lines = list(lines)
def __iter__(self):
for line in self.lines:
yield line
log = ShiftLog(LOG[2:5])
print(len(list(log)), len(list(log)))
Коли потрібен справжній клас-ітератор
Коли курсор має власний інтерфейс: наприклад, peek() — подивитися наступний елемент, не забираючи його, чи position для відновлення читання після збою. Для простого перебору досить генератора в __iter__.
Генератор зсередини¶
Генераторна функція при виклику не виконується, а створює об'єкт-генератор. Модуль inspect показує, в якому стані цей об'єкт:
from inspect import getgeneratorstate
def shift():
print("зміна почалась")
yield "перша подія"
print("між подіями")
yield "друга подія"
print("зміна закінчилась")
return "звіт готовий"
gen = shift()
print(getgeneratorstate(gen))
print(next(gen))
print(getgeneratorstate(gen))
print(next(gen))
try:
next(gen)
except StopIteration as stop:
print("StopIteration:", stop.value)
print(getgeneratorstate(gen))
GEN_CREATED
зміна почалась
перша подія
GEN_SUSPENDED
між подіями
друга подія
зміна закінчилась
StopIteration: звіт готовий
GEN_CLOSED
stateDiagram-v2
[*] --> GEN_CREATED: gen = shift()
GEN_CREATED --> GEN_RUNNING: next()
GEN_RUNNING --> GEN_SUSPENDED: yield
GEN_SUSPENDED --> GEN_RUNNING: next() / send()
GEN_RUNNING --> GEN_CLOSED: return або кінець тіла
GEN_SUSPENDED --> GEN_CLOSED: close()
GEN_CLOSED --> [*]
Три речі, яких не було в уроці 11:
- кожен
next()виконує тіло від попередньогоyieldдо наступного — тому «між подіями» друкується лише з другимnext(); returnу генераторі не віддає значення вfor, а кладе його вStopIteration.value.forце значення ігнорує; нижче побачимо, хто його забирає;- закритий генератор уже нічого не віддасть.
close() і finally: прибирання за собою¶
Генератор, що читає з мережі чи файлу, мусить закрити з'єднання, навіть якщо споживач узяв лише частину даних:
def read_events(lines):
print("відкрили з'єднання")
try:
for line in lines:
yield line
finally:
print("закрили з'єднання")
events = read_events(LOG)
print(next(events))
events.close()
print(getgeneratorstate(events))
close() кидає всередину генератора, на місці паузи, спеціальний виняток GeneratorExit. Спрацьовує finally, і генератор завершується. CPython робить те саме автоматично, коли на генератор більше немає посилань, але явне закриття — надійніше.
.send(): генератор, що приймає дані¶
Досі дані текли з генератора. Метод .send(value) передає значення в генератор: воно стає результатом виразу yield. Середній час доставки, що оновлюється після кожної доставки:
def running_average():
total = 0
count = 0
average = None
while True:
minutes = yield average
total += minutes
count += 1
average = round(total / count, 1)
avg = running_average()
print(next(avg))
for minutes in [18, 24, 37, 21]:
print(avg.send(minutes))
Рядок minutes = yield average робить дві речі: віддає назовні поточне середнє й ставить функцію на паузу, чекаючи на наступне число. Стан — total і count — живе в локальних змінних призупиненої функції, без класу й без глобальних змінних.
Перший next(avg) запускає генератор: доводить його до першого yield, де вже є кому прийняти значення. Без запуску:
Щоб не забувати запуск, його ховають у декоратор — той самий прийом, що в уроці 10:
from functools import wraps
def primed(func):
@wraps(func)
def wrapper(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return wrapper
@primed
def running_average():
total = 0
count = 0
average = None
while True:
minutes = yield average
total += minutes
count += 1
average = round(total / count, 1)
avg = running_average()
print(avg.send(18), avg.send(24))
Такий генератор, що отримує дані через send, називають сопрограмою (coroutine).
Генератор як скінченний автомат¶
В уроці 22 автомат статусів жив у класі Order. Той самий автомат можна записати генератором: стан — локальна змінна, подія приходить через send, новий стан віддається через yield.
COURIER_TRANSITIONS = {
("вільний", "picked"): "везе",
("везе", "delivered"): "вільний",
}
@primed
def courier_state():
state = "вільний"
while True:
event = yield state
new_state = COURIER_TRANSITIONS.get((state, event))
if new_state is None:
print(f" подія {event} у стані «{state}» — пропускаю")
else:
state = new_state
courier = courier_state()
for event in ["picked", "delivered", "delivered", "picked"]:
print(event, "→", courier.send(event))
picked → везе
delivered → вільний
подія delivered у стані «вільний» — пропускаю
delivered → вільний
picked → везе
Генератор чи клас? Генератор коротший, коли автомат має один вхід і один вихід: подія → стан. Клас кращий, коли станом цікавиться багато коду (order.status), є кілька дій з різними іменами (cook(), cancel()) чи історія змін. Для Order з уроку 22 клас правильний; для лічильника стану кур'єра в потоці подій — генератор.
yield from: делегування й результат¶
В уроці 11 yield from просто віддавав елементи іншого ітерабельного. Але він робить більше: передає send і close вкладеному генератору, а коли той завершується — повертає його return-значення. Так генератор-стадія може віддавати дані й наприкінці ще й звітувати:
def valid_events(lines):
broken = 0
for line in lines:
parts = line.split()
if len(parts) != 4 or not parts[2].isdigit():
broken += 1
continue
yield parts
return broken
def with_report(lines):
broken = yield from valid_events(lines)
print(f"битих рядків: {broken}")
events = list(with_report(LOG))
print(len(events), events[2])
with_report для зовнішнього коду — звичайний генератор із 10 подіями, а всередині він дізнався, скільки рядків відкинуто. Саме з yield from і сопрограм на генераторах виросли async / await — про них в уроці 28.
Алгебра itertools¶
Модуль itertools — набір «цеглинок», з яких збирають конвеєри без ручних циклів. В уроці 11 були count, chain, islice. Далі — ті, що розв'язують задачі диспетчерської.
dropwhile і takewhile: межі потоку¶
Журнал упорядкований за часом. Треба відкинути події до початку зміни й узяти першу половину зміни:
from itertools import dropwhile, takewhile
shift_events = dropwhile(lambda line: line < "18:00", LOG)
print(next(shift_events))
first_half = takewhile(lambda line: line < "18:30", dropwhile(lambda line: line < "18:00", LOG))
print(len(list(first_half)))
Рядки порівнюються як текст, а формат «ГГ:ХХ» сортується так само, як час. Чим це відрізняється від filter:
dropwhileвідкидає елементи, доки умова правдива, а потім пропускає все, вже не перевіряючи;takewhileбере елементи, доки умова правдива, і на першому хибному зупиняє потік.
Тому takewhile працює і з нескінченним потоком, а filter на ньому ніколи не закінчиться. Ціна — обидва покладаються на упорядкованість: подія 18:10, що запізнилася й прийшла після 18:31, буде відкинута takewhile разом із рештою.
groupby: групи сусідів¶
groupby(дані, key) збирає в групу сусідні елементи з однаковим ключем. Як гадаєш, скільки груп дадуть події, згруповані за кур'єром?
from itertools import groupby
groups = [(courier, len(list(group))) for courier, group in groupby(events, key=lambda event: event[1])]
print(len(groups), groups[:4])
Дев'ять груп на трьох кур'єрів: щойно кур'єр змінюється, починається нова група. groupby схожий на uniq з командного рядка — він не шукає однакові ключі по всьому потоку. Щоб отримати по групі на кур'єра, дані спершу сортують за тим самим ключем:
by_courier = sorted(events, key=lambda event: event[1])
print([(courier, len(list(group))) for courier, group in groupby(by_courier, key=lambda event: event[1])])
Але сортування потребує всіх даних у пам'яті й O(n log n). Для потоку зі змішаними ключами словник або Counter з уроку 6 — один прохід і пам'ять лише на ключі. groupby доречний, коли дані вже прийшли згруповані: журнал, відсортований за днем, файл, розбитий за кур'єром.
pairwise і accumulate: сусідні пари й наростаючий підсумок¶
Найдовша пауза між подіями — сигнал, що застосунок втрачав зв'язок. pairwise (Python 3.10+) віддає пари сусідніх елементів:
from itertools import accumulate, pairwise
def minutes(hhmm):
hours, mins = hhmm.split(":")
return int(hours) * 60 + int(mins)
times = [minutes(event[0]) for event in events]
gaps = [later - earlier for earlier, later in pairwise(times)]
print(gaps, max(gaps))
delivered = [1 if event[3] == "delivered" else 0 for event in events]
print(list(accumulate(delivered)))
accumulate — наростаючий підсумок: після кожної події видно, скільки доставок уже виконано. Це той самий «підсумок на льоту», що й running_average, лише для суми.
combinations: перебір без вкладених циклів¶
В уроці 23 ми шукали набори поїздок на ваучер 500 грн перебором з поверненням. Для наборів фіксованого розміру є готовий перебір:
from itertools import combinations
fares = [230, 150, 270, 180, 120, 410]
print([pair for pair in combinations(fares, 2) if sum(pair) == 500])
print([trio for trio in combinations(fares, 3) if sum(trio) == 500])
print(sum(1 for size in range(1, len(fares) + 1) for _ in combinations(fares, size)))
Ті самі два набори, що в уроці 23. Але combinations перебирає всі 63 непорожні підмножини — без відсікання. Для 6 поїздок це дрібниця; для 40 — понад трильйон. combinations і product замінюють вкладені цикли, коли перебрати треба все; коли гілки можна відсікати — потрібен перебір з поверненням.
Інші корисні цеглинки
zip_longest — zip, що не обрізає довший потік; chain.from_iterable — сплющити потік списків; tee — розгалузити один ітератор на кілька (з буфером у пам'яті); batched(дані, n) — пачки по n (з Python 3.12; для старіших версій напишемо самі в практиці).
Архітектура: ETL-конвеєр подій¶
Задачу диспетчерської зручно побудувати як ETL (extract — transform — load): витягти дані, перетворити, завантажити результат. Кожна стадія — функція «ітерабельне → ітерабельне», і стадії з'єднуються як труби:
flowchart TD
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
SRC["журнал<br>рядки"] --> P["parse<br>рядок → подія"]
P --> S["in_shift<br>dropwhile"]
S --> D["durations<br>пара picked–delivered"]
D --> R["report<br>середнє по кур'єрах"]
P -. "битий рядок" .-> REJ["rejected<br>окремий список"]
class SRC,P,S,D step
class R success
class REJ warning
Хто штовхає, а хто тягне¶
У конвеєрі з генераторів дані тягне споживач: report просить наступну тривалість, durations — наступну подію, і так до джерела. Сопрограми з .send() працюють навпаки — дані штовхає джерело:
flowchart LR
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
subgraph PUSH["push: сопрограми"]
direction TB
SRC2["джерело: подія прийшла"] -- "send()" --> K1["стадія"]
K1 -- "send()" --> K2["підсумок"]
end
subgraph PULL["pull: генератори"]
direction TB
C1["споживач: for / list"] -- "next()" --> G1["стадія"]
G1 -- "next()" --> SRC1["джерело"]
end
class C1,SRC2 decision
class G1,K1 step
class SRC1 step
class K2 success
Pull природний, коли дані можна попросити: файл, список, база. Push — коли дані приходять самі й треба реагувати на кожну: повідомлення від застосунку, дані з датчика. Середній час після кожної доставки — задача push; звіт наприкінці зміни — pull.
Компроміси¶
| Підхід | Пам'ять | Проходів | Хто керує | Коли |
|---|---|---|---|---|
| списки між стадіями | весь проміжний результат | скільки завгодно | код, що викликає стадії | мало даних, треба кілька проходів чи індекси |
| конвеєр генераторів | один елемент на стадію | один | споживач (pull) | великі файли, потоки, звіти |
| клас-ітератор | як генератор | один на курсор | споживач | курсор з власними методами: peek, позиція |
сопрограма .send() |
стан сопрограми | — | джерело (push) | реакція на кожну подію, автомат, ковзні підсумки |
Помилки й тести¶
- Биті дані не губимо мовчки (Zen of Python: «помилки не мають минати непомітно»). Стадія розбору відкладає биті рядки в окремий список — у промисловому ETL це «dead letter queue». Альтернатива — лічильник через
returnіyield from, як уvalid_events. - Кожна стадія тестується окремо: вона приймає будь-яке ітерабельне, тож для перевірки досить списку з двох-трьох рядків. Саме так ми писатимемо тести в уроці 26.
Практика¶
Розібраний приклад: середній час доставки по кур'єрах¶
Що тут працює:
parse— генератор-стадія: перетворює рядок на словник-подію, биті рядки відкладає вrejected.in_shift— однаdropwhile: події попередньої зміни (доставки 97 і 98) не потрапляють далі.durationsпам'ятає лише відкриті замовлення — ті, що забрали, але ще не доставили. Доставлене замовлення виходить зі словника черезpop, тож пам'ять не росте разом з потоком.report— єдине місце, де дані накопичуються, і лише як тривалості по кур'єрах.- Рядок 32 нічого не обчислює: він лише з'єднує труби. Робота починається, коли
reportпросить першу тривалість.
D-1 доставив два замовлення: 101 за 18 хв і 104 за 21 хв — середнє 19.5.
Зміни приклад: запізнення¶
Додай стадію late(pairs, limit), що пропускає далі лише доставки, довші за limit хвилин, і виведи їх для limit=30. Стадію став між durations і list, не змінюючи інших функцій.
Підказка
list(late(durations(in_shift(parse(LOG, []), minutes("18:00"))), 30)) дасть [('D-3', 37)].
Спробуй самостійно: пачки й ковзне середнє¶
- Напиши генератор
batched(iterable, n), що віддає кортежі поnелементів (останній може бути коротшим):list(batched(range(7), 3))→[(0, 1, 2), (3, 4, 5), (6,)]. Він має працювати з нескінченним потоком:next(batched(count(), 2))→(0, 1). - Напиши генератор
moving_average(values, k)— середнє останніхkзначень для кожної позиції, починаючи зk-ї:list(moving_average([18, 24, 37, 21], 2))→[21.0, 30.5, 29.0]. Не перераховуй суму вікна щоразу — ковзне вікно з уроку 12.
Підказка
- Бери пачку через
tuple(islice(iterator, n))з одного ітератораit = iter(iterable); порожня пачка — сигнал зупинитися. - Тримай
window_sum: додай нове значення, відніми те, що випало з вікна (values[i - k]), або тримай вікно вcollections.deque(maxlen=k).
Знайди помилку¶
# 1 — звіт по кур'єрах
from itertools import groupby
counts = {courier: len(list(group)) for courier, group in groupby(events, key=lambda event: event[1])}
# 2 — дві цифри з одного розбору
parsed = parse(LOG, [])
total = sum(1 for _ in parsed)
result = report(durations(parsed))
# 3 — середнє на льоту
def running_average():
total, count, average = 0, 0, None
while True:
minutes = yield average
total += minutes
count += 1
average = total / count
avg = running_average()
print(avg.send(18))
Відповіді
- Дані не відсортовані за кур'єром:
groupbyдає кілька груп на одного кур'єра, і в словнику лишиться лише остання група кожного. Треба спершуsorted(events, key=…)— або рахуватиCounter(event[1] for event in events). sumвичерпав генераторparsed;reportотримає порожній потік і поверне{}. Або створити конвеєр заново, або — якщо даних мало — один раз зробитиlist(parse(…)).- Генератор не запущено:
send(18)падає зTypeError: can't send non-None value to a just-started generator. Треба спершуnext(avg)або декоратор@primed.
Підсумок¶
| Поняття | Що запам'ятати |
|---|---|
| Клас-ітератор | __next__ віддає елемент або StopIteration; __iter__ повертає self |
| Ітерабельне й ітератор | ітерабельне на кожен iter() дає новий курсор; найпростіше — генератор у __iter__ |
| Стани генератора | GEN_CREATED → GEN_SUSPENDED ⇄ GEN_RUNNING → GEN_CLOSED |
return у генераторі |
значення йде в StopIteration.value; забирає його yield from |
close() і finally |
прибирання ресурсів, навіть якщо споживач зупинився раніше |
.send() |
значення стає результатом yield; спершу запустити next() або @primed |
| Генератор-автомат | стан у локальній змінній; добре для «подія → стан» |
dropwhile / takewhile |
межі впорядкованого потоку; takewhile зупиняє і нескінченний потік |
groupby |
групує лише сусідів — спершу сортувати за тим самим ключем |
pairwise, accumulate |
сусідні пари; наростаючий підсумок |
combinations |
повний перебір наборів фіксованого розміру, без відсікання |
| ETL-конвеєр | стадії «ітерабельне → ітерабельне», биті записи — окремо, кожна стадія тестується окремо |
| Pull і push | генератори тягнуть дані, сопрограми отримують їх через send |
Самоперевірка¶
- Чому другий
list(log)для першої версіїShiftLogпорожній? Як це виправити двома способами? - Що друкує генератор
shift()між першим і другимnext()і чому саме тоді? - Куди потрапляє значення
returnгенератора? Хто його може отримати? - Навіщо в
read_eventsблокfinally? - Що робить рядок
minutes = yield average? Чому перед першимsendпотрібенnext? - Коли автомат краще писати генератором, а коли — класом?
- Чим
takewhileвідрізняється відfilterна нескінченному потоці? - Чому
groupbyдав 9 груп на трьох кур'єрів? - Чому
durationsне накопичує всі події зміни в пам'яті?
Відповіді
- Курсор
positionживе в самому журналі й після першого проходу стоїть у кінці. Виправлення: окремий клас-курсор, який__iter__створює щоразу, або генератор у__iter__. - «між подіями» — бо кожен
next()виконує тіло від попередньогоyieldдо наступного. - У
StopIteration.value. Його отримуєyield fromяк значення виразу, або код, що сам ловитьStopIteration. - Щоб з'єднання закрилося, навіть коли споживач не дочитав до кінця й викликав
close(). - Віддає
averageназовні й чекає; значення зsendстає результатомyield. До першогоnextгенератор ще не дійшов доyield, тож приймати значення нікому. - Генератор — коли вхід один (подія) і вихід один (стан). Клас — коли стан читає багато коду, є кілька дій з іменами чи історія.
takewhileзупиняє потік на першому хибному елементі;filterперевіряє всі елементи й на нескінченному потоці не закінчиться.- Він групує лише сусідні елементи, а кур'єри в журналі йдуть упереміш.
- Він тримає лише відкриті замовлення й видаляє кожне через
pop, щойно воно доставлене.
Що далі¶
- Ноутбук заняття: Відкрити вправи в Colab Переглянути розв’язки — прогнози й вправи з перевірками: курсор з
peek, сопрограма-лічильник,batched, ковзне середнє, звіт по кур'єрах. - Наступне заняття — урок 26 «Тестування з pytest»: стадії нашого конвеєра — ідеальні кандидати на перші тести.
- Урок 28 — потоки,
multiprocessingіasyncio:async/await— нащадки.send()іyield from.
Документація і джерела¶
- Туторіал: Iterators, Generators
- Довідник мови: Generator-iterator methods —
__next__,send,throw,close itertools— усі цеглинки й «рецепти» з прикладами;inspect.getgeneratorstate- PEP 342 — Coroutines via Enhanced Generators (
send,close); PEP 380 — Syntax for Delegating to a Subgenerator (yield from) - Для охочих: Dave Beazley, «A Curious Course on Coroutines and Concurrency» — сопрограми на генераторах, конвеєри з
send, від простого до власного планувальника задач.