Deep Engineering

ЗАМЕР

bench/lambda/silent.py

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

Цитируется в статье
/ru/interview/python/lambda
Как запустить
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v late_binding.py; done

Запись прогона

Замеры для урока «lambda»

скрипт что показывает
identity.py тип, байт-код и __name__ у lambda и def; что в lambda не компилируется
late_binding.py [lambda: i for i in range(3)] даёт [2, 2, 2]; одна ячейка на три функции; чем включение отличается от for-цикла
silent.py ошибка, которая не падает: три проверки с одним порогом дают 3 вместо 6
cost.py lambda против def при вызове и в key=; operator.itemgetter против обоих

Запускать на всех версиях, которые есть:

for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v late_binding.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.7 и 3.14.7 — Clang 20.1.4, но и эти две сборки различаются между собой, причём ровно тем флагом (--with-tail-call-interp), которому «Что нового в 3.14» приписывает «a geometric mean of 3-5% faster».

В cost.py все сравнения делаются внутри одного запуска одного интерпретатора: lambda против def, и оба против itemgetter. Такое сравнение честно всегда и от тулчейна не зависит. Числа для 3.13.7 и 3.14.7 приводятся рядом как два независимых результата, а не как сравнение.

Отдельно про то, чего в cost.py НЕТ: там не измеряется sorted(DATA) без key. Он сравнивал бы кортежи по первому элементу, то есть по строке, а три измеряемые формы сортируют по числу — это разная работа, и приписывать разницу способу извлечения ключа было бы подлогом.

identity.py, late_binding.py и silent.py от тулчейна не зависят вовсе: они смотрят на типы, имена, содержимое ячеек замыкания и результат вычисления.

Разброс между запусками

cost.py — лучшее из семи прогонов. Повторные запуски дают разброс около ±3 %, и он важен для чтения результата: разница между key=lambda и key=def в него укладывается (в повторных прогонах порядок этих двух даже меняется местами), а разница с itemgetter — нет, она устойчиво в полтора раза. Поэтому в уроке сказано «неотличимо» про первую пару и названа кратность для второй.

Скрипт

32 строк
"""Ошибка, которая не падает: набор проверок, собранный в цикле.

Как и в трёх предыдущих уроках, интересна не та ошибка, что роняет программу,
а та, что молча возвращает неверный ответ.
"""
import sys
print("PY", sys.version.split()[0])

VALUES = [5, 50, 500, 5000]
THRESHOLDS = [10, 100, 1000]

# НЕВЕРНО: три проверки, но порог у всех один — последний.
broken = [lambda v: v > t for t in THRESHOLDS]

# ВЕРНО: порог зафиксирован аргументом по умолчанию.
correct = [lambda v, t=t: v > t for t in THRESHOLDS]

def report(checks, label):
    counts = [sum(1 for v in VALUES if check(v)) for check in checks]
    print(f"  {label}: прошло порогов {THRESHOLDS} -> {counts}, всего {sum(counts)}")
    return sum(counts)

print("1) сколько значений проходит каждый порог")
bad = report(broken, "неверно ")
good = report(correct, "верно   ")
print(f"   ожидалось {good}, получено {bad}; исключений не было")

# 2. Признак, по которому это видно в отладчике: у сломанных функций одна
#    ячейка замыкания на всех, у правильных замыкания нет вовсе.
print("2) ячеек замыкания у сломанных:", len({id(f.__closure__[0]) for f in broken}))
print("   замыкание у правильных:", correct[0].__closure__)