Deep Engineering

ЗАМЕР

bench/iterators/traps.py

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/python/iterators-and-generators
Как запустить
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 и глубиной стека внутри включения. Эти признаки от тулчейна не зависят вовсе.

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

Скрипт

55 строк
import sys, timeit
print("PY", sys.version.split()[0])

# PEP 479: StopIteration внутри тела генератора
def broken(it):
    while True:
        yield next(it)          # next() бросит StopIteration, когда it кончится

try:
    print("1) list(broken):", list(broken(iter([1, 2]))))
except BaseException as e:
    print(f"1) {type(e).__name__}: {e}")
    print("   __cause__:", type(e.__cause__).__name__ if e.__cause__ else None)

# правильный вариант
def fixed(it):
    while True:
        try:
            yield next(it)
        except StopIteration:
            return
print("2) list(fixed):", list(fixed(iter([1, 2]))))

# yield from против ручного цикла
setup = """
def inner(n):
    for i in range(n): yield i
def manual(n):
    for x in inner(n): yield x
def delegated(n):
    yield from inner(n)
"""
t_m = min(timeit.repeat("sum(manual(1000))", setup=setup, repeat=7, number=2000)) / 2000
t_d = min(timeit.repeat("sum(delegated(1000))", setup=setup, repeat=7, number=2000)) / 2000
print(f"3) обход 1000 через прослойку: цикл {t_m*1e6:7.1f} мкс | yield from {t_d*1e6:7.1f} мкс"
      f" | быстрее в {t_m/t_d:.2f} раза")

# исчерпание при повторном использовании
def rows():
    yield from [1, 2, 3]
r = rows()
print("4) sum:", sum(r), "| max по тому же объекту:", end=" ")
try:
    print(max(r))
except ValueError as e:
    print(f"ValueError: {e}")

# len и in
g = (i for i in range(5))
try:
    len(g)
except TypeError as e:
    print("5) len(генератор):", f"TypeError: {e}")
print("   3 in g:", 3 in g, "| что осталось после проверки:", list(g))