Deep Engineering

MEASUREMENT

bench/iterators/protocol.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/interview/python/iterators-and-generators
How to run it
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v protocol.py; done

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Замеры для урока «Итераторы и генераторы»

скрипт что показывает
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 и глубиной стека внутри включения. Эти признаки от тулчейна не зависят вовсе.

Сравнение «генератор против списка» при этом остаётся честным всегда: оно делается внутри одного запуска одного интерпретатора.

Script

45 lines
import sys
print("PY", sys.version.split()[0])

# 1. итерируемое против итератора
xs = [1, 2, 3]
it = iter(xs)
print("1) iter(list) is list:", iter(xs) is xs)
print("   iter(iterator) is iterator:", iter(it) is it)
print("   у списка есть __next__:", hasattr(xs, "__next__"))
print("   у итератора есть __iter__:", hasattr(it, "__iter__"))

# 2. исчерпание
print("2) первый проход:", list(it), "второй проход:", list(it))
print("   а список можно обойти дважды:", list(xs), list(xs))

# 3. генератор — это итератор
def g():
    yield 1
    yield 2
gen = g()
print("3) iter(gen) is gen:", iter(gen) is gen)
print("   тип функции с yield:", type(g).__name__, "| тип вызова:", type(gen).__name__)

# 4. тело не выполняется при вызове
log = []
def h():
    log.append("тело пошло")
    yield 1
obj = h()
print("4) после вызова h():", log)
next(obj)
print("   после первого next:", log)

# 5. состояния генератора
import inspect
def k():
    yield 1
    yield 2
kk = k()
print("5) до next:", inspect.getgeneratorstate(kk))
next(kk)
print("   после next:", inspect.getgeneratorstate(kk))
list(kk)
print("   после исчерпания:", inspect.getgeneratorstate(kk))