ЗАМЕР
bench/iterators/states.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/async-await
- Как запустить
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v protocol.py; done
Запись прогона
Замеры для урока «Итераторы и генераторы»
| скрипт | что показывает |
|---|---|
protocol.py |
iter(x) is x для списка и итератора, исчерпание, состояния генератора |
silent.py |
ошибка, которая не падает: два прохода по аргументу дают 0 вместо 19 |
memory_time.py |
пик памяти (tracemalloc) и время для списка против генератора на 1 000 000 и на 10 элементах |
traps.py |
PEP 479 (RuntimeError и __cause__), yield from против ручного цикла, in и len на генераторе |
versions.py |
запись __future__.generator_stop и возврат значения из close() |
inlining.py |
PEP 709 структурно: <listcomp>, MAKE_FUNCTION и глубина стека — без секундомера |
Запускать на всех версиях, которые есть:
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v protocol.py; done
Практика урока
practice.py — источник ответов двух практических задач урока, а runs/practice.txt —
дословная запись его прогона. Ответ задачи не сочиняется: сборка сверяет
заявленное с этой записью (scripts/validate-practice.mjs) и не проходит,
если они разошлись.
Прогон снят 30.08.2026 на CPython 3.13.7 (Clang 20.1.4). Абсолютные числа — этой машины; переносится кратность, и задача «во сколько раз» стоит именно на ней.
Кратность устойчива только потому, что формы меряются ВПЕРЕМЕЖКУ: в каждом круге меряются все, минимум для каждой берётся по кругам. Пока замеры шли подряд, просадка машины в окне одной формы целиком доставалась ей, и отношение гуляло в полтора раза от запуска к запуску (измерено на декораторах: 5,9 / 7,1 / 7,5 / 9,2). После перехода на чередование расхождение между прогонами не выходит за несколько процентов. Перезаписывать запись прогона имеет смысл только вместе с проверкой задачи: если после перезапуска ответ изменился, менять нужно задачу, а не файл.
Правило, из-за которого половина замеров сделана структурно
3.11 и 3.12 собраны GCC 13.3.0, а 3.13 и 3.14 — Clang 20.1.4. Сравнивать время между этими парами нельзя: разница сборок перекрывает разницу версий; это общее правило замеров, а не оговорка одного этого каталога.
Поэтому эффект PEP 709 в уроке показан не секундомером, а inlining.py:
наличием объекта кода <listcomp>, инструкции MAKE_FUNCTION и глубиной
стека внутри включения. Эти признаки от тулчейна не зависят вовсе.
Сравнение «генератор против списка» при этом остаётся честным всегда: оно делается внутри одного запуска одного интерпретатора.
Скрипт
149 строк"""Состояния генератора и корутины — по опросу самого интерпретатора.
ЗАЧЕМ ЭТОТ ФАЙЛ. Про генератор в уроке сказано всё: вызов создаёт объект,
`next` доводит до `yield`, исчерпание даёт `StopIteration`, `close` завершает.
Но сказано это словами и по кускам, а машина состояний в голове читателя при
этом не собирается: где объект находится МЕЖДУ вызовами и чем «исчерпан»
отличается от «закрыт», из отдельных абзацев не видно.
Состояние — не метафора: `inspect.getgeneratorstate` и
`inspect.getcoroutinestate` возвращают его строкой. Значит, картинку можно не
рисовать по памяти, а снять прогоном: каждый переход здесь печатается вместе с
состоянием до и после.
ЧТО ЗДЕСЬ ЕСТЬ. Блок 1 — жизнь генератора целиком, включая `send`, `throw` и
`close`. Блок 2 — то же для корутины. Блок 3 — разница между объектом-корутиной
и задачей: у них разные состояния и разные владельцы, и путают их чаще всего.
ЗАПУСК: python3.13 bench/iterators/states.py
Вывод по версиям: runs/states-3.11.txt и соседние.
"""
import asyncio
import inspect
import sys
def show(title: str) -> None:
print()
print(title)
print("-" * len(title))
def row(event: str, state: str, extra: str = "") -> None:
print(f" {event:<34} {state:<14} {extra}")
# ------------------------------------------------------------------ 1
def block1() -> None:
show("1. Генератор: состояние после каждого действия")
log: list[str] = []
def counter():
log.append("тело начало выполняться")
received = yield 1
log.append(f"после первого yield получено: {received!r}")
yield 2
log.append("тело дошло до конца")
gen = counter()
row("вызов counter()", inspect.getgeneratorstate(gen), "тело ещё не выполнялось")
row("next(gen) -> " + str(next(gen)), inspect.getgeneratorstate(gen), "приостановлен на yield")
row("gen.send('привет') -> " + str(gen.send("привет")), inspect.getgeneratorstate(gen))
try:
next(gen)
except StopIteration:
row("next(gen) -> StopIteration", inspect.getgeneratorstate(gen), "тело кончилось")
# Второй генератор — чтобы показать закрытие, а не исчерпание: это разные
# пути в одно и то же состояние, и различить их можно только так.
other = counter()
next(other)
other.close()
row("gen2.close()", inspect.getgeneratorstate(other), "закрыт, а не исчерпан")
# Третий — состояние ВНУТРИ работы. Снаружи его не увидеть: пока генератор
# выполняется, управление у него, и спросить может только он сам.
def introspect():
yield inspect.getgeneratorstate(inner)
inner = introspect()
row("состояние изнутри тела", next(inner), "единственный способ увидеть RUNNING")
print()
for line in log:
print(" журнал:", line)
# ------------------------------------------------------------------ 2
def block2() -> None:
show("2. Корутина: те же четыре состояния, другие имена")
async def work():
await asyncio.sleep(0)
return "готово"
coro = work()
row("вызов work()", inspect.getcoroutinestate(coro), "тело не выполнялось")
async def drive():
task = asyncio.ensure_future(coro)
await asyncio.sleep(0)
row("после первого шага", inspect.getcoroutinestate(coro), "приостановлена на await")
result = await task
row("после завершения", inspect.getcoroutinestate(coro), f"результат: {result!r}")
asyncio.run(drive())
# Корутина, которую никто не запускал: закрыть её можно, и состояние
# отличается от завершённой — та же пара «исчерпан / закрыт», что у
# генератора.
never = work()
never.close()
row("coro2.close() без запуска", inspect.getcoroutinestate(never), "закрыта, не завершена")
# ------------------------------------------------------------------ 3
def block3() -> None:
show("3. Объект-корутина и задача — разные вещи")
async def work():
await asyncio.sleep(0.01)
return 42
async def main():
coro = work()
row("тип объекта из вызова", type(coro).__name__, "тело не выполняется")
row("кто его выполняет", "никто", "пока его не отдали циклу событий")
task = asyncio.create_task(coro)
row("тип после create_task", type(task).__name__, "теперь у него есть исполнитель")
row("состояние корутины", inspect.getcoroutinestate(coro))
row("задача завершена?", str(task.done()), "цикл ещё не доходил до неё")
result = await task
row("после await задачи", str(task.done()), f"результат: {result}")
row("состояние корутины", inspect.getcoroutinestate(coro))
# Задачу можно спросить второй раз — корутину нельзя.
row("task.result() ещё раз", str(task.result()), "результат хранится в задаче")
try:
await coro
except RuntimeError as exc:
row("await той же корутины", type(exc).__name__, "корутину нельзя ждать дважды")
asyncio.run(main())
def main() -> None:
print(f"Python {sys.version.split()[0]} ({sys.implementation.name})")
block1()
block2()
block3()
if __name__ == "__main__":
main()