Итераторы, генераторы и корутины

Протокол итерации, генераторы и yield, модуль itertools, сопрограммы, async и await

Вячеслав Федоров

Лекция 7

Разработка и применение ПО

в физических исследованиях

yield

Содержание

От протокола итерации до async и await

01

Итераторы

02

Генераторы

03

Модуль itertools

04

Сопрограммы

05

async и await

Цель лекции

Не вычислять всё сразу: элементы по одному, вычисления по запросу, ожидание без блокировки

Итератор выдаёт элементы по одному, генератор приостанавливает функцию на yield и продолжает её с места остановки, сопрограмма принимает значения извне, а цикл событий переключается между сопрограммами, пока они ожидают. Эти механизмы позволяют обрабатывать данные, не помещающиеся в память, описывать бесконечные последовательности и в одном потоке одновременно ожидать ответов нескольких приборов.

Итераторы

Протокол итерации, цикл for, собственные итераторы, итераторы стандартной библиотеки

01

Итераторы

Генераторы

Модуль itertools

Сопрограммы

async и await

Устройство

цикла for

for x in xs:
    process(x)

выполняется как

it = iter(xs)
while True:
    try:
        x = next(it)
    except StopIteration:
        break
    process(x)

ВызовМетод
iter(xs)xs.__iter__()
next(it)it.__next__()
элементов нетraise StopIteration

>>> dis.dis('for x in xs: pass')  # 3.12
GET_ITER        # iter(xs)
FOR_ITER        # next(it)
STORE_NAME x
JUMP_BACKWARD
END_FOR

Пояснение

StopIteration перехватывает сам цикл: в коде исключение не видно.

Итерируемый объект

и итератор

xs = [1.5, 2.5, 4.0]

it = iter(xs)

итератор: __next__ и __iter__

next(it) × 3

1.5, 2.5, 4.0

next(it)

StopIteration при каждом вызове

ИтерируемыйИтератор
методы__iter__ → новый итератор__next__; __iter__ → self
проходовсколько угодноодин
примерыlist, str, dict, rangeiter(list), файл, map, zip

Важно

Итератор однопроходный: после StopIteration каждый следующий next снова даёт StopIteration.

Пояснение

Проверка: isinstance(x, collections.abc.Iterator) отличает итератор от итерируемого объекта.

Демонстрация:

итератор списка

01

Итератор списка: iter(xs)

02

Элементы и дважды StopIteration

03

iter(it) is it: True

04

Пустой list(it) и полный list(xs)

05

range: не итератор

06

Байт-код: GET_ITER и FOR_ITER

Что наблюдается

Исчерпанный итератор каждый раз даёт StopIteration; список выдаёт новый итератор на каждый проход.

root@lab — запись со стенда

Собственный

итератор

class Linspace:
    def __init__(self, a, b, n):
        self.a, self.n = a, n
        self.h = (b - a) / (n - 1)
    def __iter__(self):
        return LinspaceIterator(self.a, self.h, self.n)
 
class LinspaceIterator:
    def __init__(self, a, h, n):
        self.a, self.h, self.n, self.i = a, h, n, 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.i == self.n:
            raise StopIteration
        self.i += 1
        return self.a + (self.i - 1) * self.h

Пояснение

Счётчик обхода i хранится в итераторе: каждый for получает новый итератор и начинает с первой точки.

Важно

Если __iter__ контейнера вернёт self и счётчик будет храниться в нём, второй проход окажется пустым.

Для физика

Linspace — упрощённый аналог numpy.linspace. Точка вычисляется как a + i·h, а не накоплением: ошибки округления не растут с номером точки.

Другие способы

итерации

МеханизмКак работаетПрименение
__getitem__перебор индексов 0, 1, 2… до IndexErrorстарые классы-последовательности
iter(f, граница)вызовы f() до граничного значениячтение блоками, опрос
__contains__x in obj; без метода — перебор через __iter__проверка принадлежности

from functools import partial
 
with open('run042.dat', 'rb') as f:
    for block in iter(partial(f.read, 4096), b''):
        process(block)

Важно

x in iterator расходует итератор: элементы до найденного включительно уже не вернуть.

Демонстрация:

протоколы итерации

01

Классы LinspaceIterator и Linspace

02

Два прохода по одной сетке

03

0.5 in it: расход итератора

04

Класс только с __getitem__

05

Чтение блоками: iter с границей b''

Что наблюдается

Сетка обходится дважды; после 0.5 in it итератор выдаёт только 0.75 и 1.0; класс без __iter__ перебирается по индексам.

root@lab — запись со стенда

Итераторы

стандартной библиотеки

ОбъектИтератор?Повторный обход
list, tuple, str, dict, setнет: итерируемыйда
range(n)нет: ленивая последовательностьда; есть len и индексы
dict.keys(), values(), items()нет: представленияда; текущее содержимое словаря
open(path)данет: позиция в файле
map, filter, zip, enumerate, reversedданет
генераторданет

Пояснение

zip(..., strict=True) (Python 3.10) проверяет равенство длин и при расхождении возбуждает ValueError.

Важно

Результат map или zip, использованный дважды, во второй раз пуст.

Генераторы

yield, состояния генератора, генераторные выражения, конвейеры, бесконечные последовательности, yield from

02

Итераторы

Генераторы

Модуль itertools

Сопрограммы

async и await

Генераторная

функция

def countdown(n):
    print('старт')
    while n > 0:
        yield n
        n -= 1
    return 'пуск'
 
g = countdown(3)   # тело не выполняется
next(g)            # старт, 3
next(g), next(g)   # (2, 1)
next(g)            # StopIteration: пуск

Пояснение

Генератором можно написать __iter__: «for i in range(self.n): yield self.a + i * self.h» заменяет класс LinspaceIterator.

g = countdown(3)

объект-генератор без выполнения тела

next(g)

печать «старт», выполнение до yield: 3

next(g)

продолжение после yield: 2

return 'пуск'

StopIteration('пуск')

Состояния

генератора

GEN_CREATED

GEN_RUNNING

GEN_SUSPENDED

GEN_CLOSED

next()

yield

next(), send(), throw()

close()

return или исключение

АтрибутЧто показывает
inspect.getgeneratorstate(g)одно из четырёх состояний
g.gi_frameкадр: f_locals, f_lineno; None после завершения
g.gi_runningTrue во время выполнения тела
g.gi_yieldfromподгенератор при yield from

Демонстрация:

состояния генератора

01

countdown: print, yield и return

02

Вызов без выполнения тела: GEN_CREATED

03

Первый next: «старт» и 3

04

Состояние и локальные переменные

05

StopIteration со значением return

06

PEP 479: next внутри генератора

Что наблюдается

«старт» — при первом next; return передан в StopIteration: пуск; StopIteration внутри firsts стала RuntimeError.

root@lab — запись со стенда

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

выражения

squares = [x * x for x in range(100_000)]
lazy = (x * x for x in range(100_000))
total = sum(x * x for x in range(100_000))

Python 3.12СписокГенератор
sys.getsizeof800 984 байта200 байт
пик памяти при sum4,0 МБ464 байта
повторный обходданет
len и индексыданет

Важно

Первый источник вычисляется сразу, остальное — при обходе: условие читает переменные в момент перебора.

data = [0, 2, 5, 12]
threshold = 1
g = (x for x in data if x > threshold)
threshold = 10
list(g)          # [12]

Конвейер

генераторов

def read_lines(path):
    with open(path) as f:
        yield from f
 
def parse(lines):
    for line in lines:
        if not line.strip() or line.startswith('#'):
            continue
        t, channel, value = line.split()
        yield float(t), channel, float(value)
 
def select(events, name):
    for t, channel, value in events:
        if channel == name:
            yield t, value
 
lines = read_lines('run042.log')
bpm = select(parse(lines), 'BPM01')
print(max(value for t, value in bpm))

read_lines('run042.log')

строки файла по одной

parse

разбор строки в событие

select

события канала BPM01

max(...)

результат; в памяти — одно событие

Демонстрация:

конвейер генераторов

01

Стадии: чтение, разбор, отбор

02

Конвейер без чтения строк

03

next: строка через все стадии

04

next: отбрасывание строки BPM02

05

next со значением по умолчанию

06

Размер списка и генератора

Что наблюдается

Строки читаются по одной и по запросу; getsizeof списка — 800 984 байта (одни ссылки), генератора — 200.

root@lab — запись со стенда

Бесконечные

последовательности

def oscillator(x, v, dt, w=1.0):
    t = 0.0
    while True:
        yield t, x, v
        v -= w * w * x * dt
        x += v * dt
        t += dt
 
states = oscillator(1.0, 0.0, 0.01)
first = list(islice(states, 3))
t0 = next(t for t, x, v in states if x < 0)

Для физика

Полунеявная схема Эйлера: сначала скорость, затем координата по новой скорости; энергия колеблется, а не растёт.

ПотребительОстановка
islice(g, n)после n элементов
takewhile(cond, g)на первом ложном условии
next(s for s in g if cond)на первом подходящем
for ... breakпо любому условию

Пояснение

Генератор описывает правило перехода к следующему состоянию, а момент остановки выбирает вызывающий код.

Демонстрация:

генератор состояний

01

Генератор состояний осциллятора, ω = 1, dt = 0,01

02

Первые три состояния через islice с печатью

03

Продолжение с четвёртого состояния

04

Первое пересечение нуля: t ≈ π/2

05

Отклонение энергии за 10 000 шагов

Что наблюдается

Координата впервые становится отрицательной при t = 1,57 (π/2 = 1,5708); за 10 000 шагов x² + v² отклоняется от 1 не больше чем на 0,00503 = dt/(2 − dt).

root@lab — запись со стенда

yield from

и делегирование

class Node:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left, self.right = left, right
 
def walk(node):
    if node is not None:
        yield from walk(node.left)
        yield node.value
        yield from walk(node.right)
 
tree = Node(5, Node(3, Node(1)), Node(8))
list(walk(tree))     # [1, 3, 5, 8]

01

Выдача наружу

всех значений подгенератора g

02

Передача внутрь

значений send и исключений throw

03

Возврат результата

значения return генератора g

r = yield from sub()   # r — значение return из sub()

Завершение

генератора

СитуацияРезультат
return значениеStopIteration(значение): результат для yield from, не для for
StopIteration в теле (PEP 479, 3.7)RuntimeError: generator raised StopIteration
next(g) внутри самого gValueError: generator already executing
досрочное прерывание обходавыполнение finally и with при close() или сборке мусора

def firsts(groups):
    for g in groups:
        yield next(iter(g))        # [] → RuntimeError
 
def firsts(groups):
    for g in groups:
        yield next(iter(g), None)

Важно

Прерванный обход генератора с открытым файлом закрывают явно: g.close() или contextlib.closing(g).

Модуль itertools

Бесконечные итераторы, срезы, объединение, группировка, комбинаторика

03

Итераторы

Генераторы

Модуль itertools

Сопрограммы

async и await

Бесконечные итераторы

и срезы

ФункцияПримерРезультат
count(start, step)count(0.0, 0.25)0.0, 0.25, 0.5, …
cycle(it)cycle('AB')A, B, A, B, …
repeat(x, n)repeat(0, 3)0, 0, 0
islice(it, start, stop, step)islice(count(), 2, 8, 2)2, 4, 6
takewhile(pred, it)takewhile(lambda t: t < 1, ts)до первого ложного
dropwhile(pred, it)dropwhile(lambda x: x < 3, [1, 4, 2, 5])4, 2, 5
compress(it, mask)compress('ABCD', [1, 0, 1, 0])A, C
filterfalse(pred, it)filterfalse(lambda x: x % 2, range(6))0, 2, 4

Пояснение

Функции модуля написаны на C и ленивы: результат — итератор, элемент вычисляется по запросу.

Объединение

и группировка

ФункцияЧто делает
chain(a, b), chain.from_iterable(xs)последовательное соединение источников
zip_longest(a, b, fillvalue=None)соединение до конца самого длинного источника
groupby(it, key)группы соседних элементов с одним ключом
pairwise(it) — 3.10соседние пары (a, b), (b, c), …
batched(it, n) — 3.12кортежи по n элементов
accumulate(it, func, initial=x)накопленные суммы, бегущий максимум
starmap(f, pairs)f(*pair) для каждой пары
tee(it, n)n независимых копий одного итератора

Важно

groupby объединяет только соседние элементы: данные сначала сортируют по тому же ключу.

Демонстрация:

itertools для событий

01

count и islice: сетка с шагом 0,25

02

chain: соединение источников

03

groupby без сортировки

04

groupby после sorted

05

pairwise: интервалы времени

06

batched и accumulate

Что наблюдается

Без сортировки BPM02 попал в две группы; после sorted значения собраны по каналам; интервалы — 0,12, 0,19 и 0,08.

root@lab — запись со стенда

Комбинаторика

и перебор параметров

ФункцияПеребираетВариантов
product(A, B)все пары (a, b)|A|·|B|
product(A, repeat=k)кортежи длины kn**k
permutations(A, k)упорядоченные выборкиn!/(n−k)!
combinations(A, k)неупорядоченные выборкиC(n, k)
combinations_with_replacement(A, k)выборки с повторениемC(n+k−1, k)

for T, B in product(temps, fields):
    run_simulation(T, B)
 
pairs = combinations(detectors, 2)
math.comb(32, 2)    # 496 пар

Для физика

product перебирает сетку параметров моделирования без вложенных циклов; combinations даёт пары детекторов для поиска совпадений.

Демонстрация:

комбинаторика

01

product: сетка 2 × 3 значений параметров

02

combinations: пары детекторов A–D

03

permutations и math.perm

04

math.comb(32, 2) и прямой подсчёт

05

product: чтение входного генератора целиком до первого результата

Что наблюдается

Сетка содержит 6 точек, четыре детектора — 6 пар, 32 детектора — 496. Генератор noisy выдал все три значения ещё при создании product.

root@lab — запись со стенда

Рецепты

и ограничения

from collections import deque
from itertools import islice
 
def moving_average(xs, n):
    it = iter(xs)
    window = deque(islice(it, n - 1), maxlen=n)
    for x in it:
        window.append(x)
        yield sum(window) / n
 
list(moving_average([1, 2, 3, 4, 5], 3))
# [2.0, 3.0, 4.0]

Для физика

Скользящее среднее сглаживает поток отсчётов: в памяти только n последних значений.

ФункцияОграничение
product, combinationsчтение входов целиком
teeхранение элементов до чтения всеми копиями
groupbyгруппа — только до перехода к следующей
isliceбез отрицательных индексов

Пояснение

Рецепты sliding_window, roundrobin, unique_everseen — в документации itertools; готовые реализации — в пакете more-itertools.

Сопрограммы

send, throw и close, yield from как канал, генераторы как менеджеры контекста

04

Итераторы

Генераторы

Модуль itertools

Сопрограммы

async и await

Метод

send

def stats():
    n, mean, m2 = 0, 0.0, 0.0
    result = None
    while True:
        x = yield result
        n += 1
        d = x - mean
        mean += d / n
        m2 += d * (x - mean)
        result = n, mean, m2 / (n - 1) if n > 1 else 0.0

Для физика

Формулы Уэлфорда: среднее и дисперсия потока измерений без хранения выборки и без вычитания близких больших сумм.

s = stats()

создание сопрограммы

next(s)

запуск до первого yield; результат None

s.send(2.0)

x = 2.0; результат (1, 2.0, 0.0)

s.send(4.0)

x = 4.0; результат (2, 3.0, 2.0)

Демонстрация:

сопрограмма статистики

01

Сопрограмма stats: n, среднее, M2

02

send до запуска: TypeError

03

next: запуск до первого yield

04

send(2.0), send(4.0), send(6.0)

Что наблюдается

До первого next сопрограмма значений не принимает; каждый send возвращает число отсчётов, среднее и дисперсию — после трёх значений (3, 4.0, 4.0).

root@lab — запись со стенда

Методы

throw и close

def counter():
    n = 0
    try:
        while True:
            try:
                yield n
                n += 1
            except ValueError:
                n = 0
    finally:
        print('закрытие, n =', n)

ВызовЧто происходит
g.throw(exc)исключение в точке yield; перехваченное — работа до следующего yield
g.close()GeneratorExit в точке yield; выполнение finally и завершение генератора
yield после GeneratorExitRuntimeError: generator ignored GeneratorExit
g.throw(тип, значение, tb)форма с тремя аргументами: устаревшая с 3.12

Пояснение

close() у ещё не запущенного генератора тело не выполняет: finally не срабатывает.

yield from

как канал

def averager():
    total, n = 0.0, 0
    while True:
        x = yield
        if x is None:
            return total / n
        total, n = total + x, n + 1
 
def collect(results, name):
    while True:
        results[name] = yield from averager()
 
results = {}
c = collect(results, 'BPM01')
next(c)
for x in (1.25, 1.5, 1.75):
    c.send(x)
c.send(None)      # конец серии
results           # {'BPM01': 1.5}

c.send(1.25)

collect

передача значения через yield from

averager

x = yield: x = 1.25

c.send(None)

return 1.5 → results['BPM01']

Демонстрация:

управление генератором

01

Счётчик с try и finally

02

Три next: 0, 1, 2

03

throw(ValueError): сброс в 0

04

close: GeneratorExit и finally

05

yield from: значение return

06

yield from как канал: averager и collect

Что наблюдается

throw сбросил счётчик без завершения генератора; close выполнил finally; через yield from среднее серии попало в results.

root@lab — запись со стенда

Генератор как

менеджер контекста

import contextlib, time
 
@contextlib.contextmanager
def timer(label):
    t0 = time.perf_counter()
    try:
        yield
    finally:
        dt = time.perf_counter() - t0
        print(f'{label}: {dt:.2f} с')
 
with timer('подгонка'):
    fit(data)

код до yield

__enter__: вход в блок with

yield значение

значение для as

finally после yield

__exit__: выход, в том числе по ошибке

Важно

Исключение из блока with возникает в точке yield: без try/finally код после yield не выполнится.

Демонстрация:

менеджер контекста

01

Менеджер timer через contextmanager

02

Блок без ошибок: время суммирования

03

Блок с ZeroDivisionError: выполнение finally и выход исключения наружу

Что наблюдается

Время напечатано в обоих случаях; ZeroDivisionError прошла через генератор и вышла из блока with.

root@lab — запись со стенда

async и await

Ожидание в одном потоке, цикл событий, нативные сопрограммы, задачи и тайм-ауты, асинхронная итерация

05

Итераторы

Генераторы

Модуль itertools

Сопрограммы

async и await

Ожидание

вместо вычисления

последовательно: 1,6 с

A 0,3 с

B 0,5 с

C 0,8 с

одновременно: 0,8 с

A

B

C

Тип задачиУзкое местоСредство
I/O-bound: сеть, приборыожидание ответаasyncio, потоки
чтение и запись файловдискпотоки, asyncio.to_thread
CPU-bound: расчётпроцессорпроцессы, NumPy, numba

Важно

Асинхронность ускоряет ожидание, а не вычисление: поток один, и долгий расчёт останавливает все задачи.

Цикл событий

на генераторах

from collections import deque
 
def task(name, n):
    for i in range(n):
        print(name, 'шаг', i)
        yield
 
def run(tasks):
    queue = deque(tasks)
    while queue:
        t = queue.popleft()
        try:
            next(t)
        except StopIteration:
            continue
        queue.append(t)

deque([A, B])

очередь готовых задач

next(A)

шаг задачи A до yield

queue.append(A)

A в конце: deque([B, A])

StopIteration

удаление задачи из очереди

Пояснение

Задачи переключаются только в точках yield: многозадачность кооперативная.

Демонстрация:

планировщик задач

01

Задача-генератор: yield после каждого шага

02

Планировщик: очередь deque и next по кругу

03

Две задачи с тремя и двумя шагами

04

Удаление завершённой задачи из очереди

Что наблюдается

Шаги приборов A и B чередуются в одном потоке; после двух шагов B в очереди остаётся только A.

root@lab — запись со стенда

async def

и

await

import asyncio
 
async def poll(name, delay):
    await asyncio.sleep(delay)  # ожидание
    return name, delay
 
async def main():
    return await poll('A', 0.3)
 
asyncio.run(main())    # ('A', 0.3)

Важно

Вызов poll(...) без await лишь создаёт объект-сопрограмму: RuntimeWarning: coroutine 'poll' was never awaited.

Генераторная сопрограммаНативная: 3.5, PEP 492
def и yieldasync def
yield from futureawait future
@asyncio.coroutine — удалён в 3.11без декоратора
loop.run_until_complete(main())asyncio.run(main()) — 3.7

Задачи

и тайм-ауты

asyncioНазначение
run(main())цикл событий для программы
await gather(a, b, c)одновременное ожидание; результаты по порядку
create_task(c)запуск сопрограммы в фоне
TaskGroup() — 3.11группа задач; отмена остальных при ошибке одной
timeout(s) — 3.11ограничение времени: TimeoutError
to_thread(f, ...) — 3.9блокирующая функция в отдельном потоке

DEVICES = {'A': 0.3, 'B': 0.5, 'C': 0.8}
 
async def main():
    async with asyncio.timeout(2.0):
        async with asyncio.TaskGroup() as tg:
            tasks = [tg.create_task(poll(n, d))
                     for n, d in DEVICES.items()]
    return [t.result() for t in tasks]

Для физика

Общее время опроса — время самого медленного прибора, а не сумма задержек.

Демонстрация:

одновременное ожидание

01

Сопрограмма poll с await asyncio.sleep

02

Вариант blocking с time.sleep

03

gather трёх опросов poll

04

Тот же gather с blocking

05

Тайм-аут 0,6 с на опросы A и C

06

Сопрограмма без await: предупреждение при удалении

Что наблюдается

С await задержки перекрываются, и общее время равно наибольшей — 0,8 с; time.sleep блокирует цикл событий, и задержки складываются в 1,6 с.

root@lab — запись со стенда

Асинхронная

итерация

СинхронноАсинхронно
__iter__, __next____aiter__, __anext__
StopIterationStopAsyncIteration
for, withasync for, async with
iter(), next()aiter(), anext() — 3.10
def с yieldasync def с yield — 3.6

async def readings(dt, n):
    for i in range(n):
        await asyncio.sleep(dt)
        yield round((i + 1) * dt, 1)
 
async def consume():
    async for t in readings(0.1, 3):
        print('отсчёт', t)
 
asyncio.run(consume())

Пояснение

Пока источник ожидает следующего элемента (await внутри __anext__), цикл событий выполняет другие задачи; источник без await управление не отдаёт.

Демонстрация:

TaskGroup и async for

01

TaskGroup: опрос A и отказ прибора B

02

except* ConnectionError: список ошибок

03

Отмена опроса A после ошибки B

04

Асинхронный генератор отсчётов с интервалом 0,1 с

05

async for: три отсчёта по мере поступления

Что наблюдается

Ошибка B отменила опрос A и пришла в ExceptionGroup; async for получил отсчёты 0.1, 0.2 и 0.3.

root@lab — запись со стенда

Выбор

средства

ЗадачаСредство
перебор данных по одному элементуитератор, генератор
конвейер обработки потокацепочка генераторов, itertools
состояние, меняющееся по входным даннымсопрограмма с send или класс
действия до и после блокаcontextlib.contextmanager
ожидание сети и приборовasyncio: gather, TaskGroup, timeout
расчёт на всех ядрахmultiprocessing, NumPy, numba
блокирующие вызовы и файлы в асинхронном кодеasyncio.to_thread

Важно

Блокирующие вызовы в асинхронном коде недопустимы: time.sleep или синхронный запрос останавливает весь цикл событий.

Итоги

лекции

01

Цикл for вызывает iter и next до StopIteration; итератор однопроходный, итерируемый объект выдаёт новый итератор на каждый проход.

02

Функция с yield возвращает генератор: тело выполняется по шагам, а состояние хранится в кадре между вызовами.

03

Генераторные выражения и конвейеры обрабатывают данные по одному элементу с постоянным расходом памяти.

04

itertools даёт ленивые бесконечные итераторы, срезы, группировку и комбинаторику без промежуточных списков.

05

send, throw и close превращают генератор в сопрограмму; yield from соединяет её с подгенератором.

06

async def и await — нативные сопрограммы: цикл событий переключает задачи, пока они ожидают.

Упражнения для самопроверки

01

Класс Linspace с методом __iter__ в виде генератора; два прохода и сравнение последней точки при накоплении шага и при a + i·h для миллиона точек

02

Конвейер для журнала событий: чтение, отбор по каналу, пересчёт в физические единицы и статистика Уэлфорда через send

03

Скользящее среднее и скользящая медиана на deque; проверка на синусоиде с шумом

04

Генераторы шагов осциллятора по явной и полунеявной схемам Эйлера; период по пересечениям нуля через pairwise и рост энергии

05

Асинхронный опрос пяти имитированных приборов: TaskGroup, общий тайм-аут, отказ одного прибора и обработка ExceptionGroup

Главы книги: «Итераторы, генераторы и корутины», «Асинхронность» · упражнения для самопроверки, без сдачи

Источники

1

Книга курса: главы «Итераторы, генераторы и корутины» и «Асинхронность»

phys-dev.github.io/soft-dev-book

2

Лебедев С. Итераторы, генераторы и модуль itertools. – Computer Science Center

youtube.com/watch?v=snJhrhD9Sg8

3

Лебедев С. Многопоточность и GIL. – Computer Science Center

youtube.com/watch?v=nR8WhdcRJwM

4

itertools — документация Python

docs.python.org/3/library/itertools.html

5

asyncio — документация Python

docs.python.org/3/library/asyncio.html

6

PEP 255, 342, 380, 479, 492, 525 — генераторы, сопрограммы, yield from, StopIteration, async и await, асинхронные генераторы

peps.python.org

7

Beazley D. A Curious Course on Coroutines and Concurrency. – PyCon, 2009

dabeaz.com/coroutines

8

Ramalho L. Fluent Python. – 2nd ed. – O'Reilly, 2022

Спасибо за внимание

Вопросы

phys-dev.github.io/soft-dev-book

await