ЗАМЕР
bench/lambda/cost.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 — нет, она устойчиво в полтора раза.
Поэтому в уроке сказано «неотличимо» про первую пару и названа кратность для
второй.
Скрипт
52 строк"""Стоит ли lambda дороже def, и что дороже — key=lambda или itemgetter.
ОБЩЕЕ ПРАВИЛО ЗАМЕРОВ: время между версиями не сравнивается вообще.
Сравнивается только измеренное внутри одного запуска одного интерпретатора —
все сравнения ниже такие. Числа 3.13.7 и 3.14.0rc2 приводятся рядом как два
независимых результата, а не как сравнение.
"""
import sys, timeit, operator, random
N = 10_000
REPEAT = 60
def named(x):
return x[1]
anon = lambda x: x[1] # noqa: E731
getter = operator.itemgetter(1)
random.seed(20260821)
DATA = [(f"k{i}", random.randint(0, 1_000_000)) for i in range(200)]
def measure(stmts, setup, number):
"""Минимум по кругам; в каждом круге меряются все формы — см. докстринг."""
best = [float("inf")] * len(stmts)
for _ in range(REPEAT):
for i, stmt in enumerate(stmts):
best[i] = min(best[i], timeit.timeit(stmt, setup=setup, number=number) / number)
return best
setup = "from __main__ import named, anon, getter, DATA"
print("PY", sys.version.split()[0], f"| лучшее из {REPEAT} чередующихся кругов")
DIRECT = [("def", "named(('k', 1))"), ("lambda", "anon(('k', 1))")]
print(" вызов напрямую (по", f"{N:,}", "итераций):")
for (label, _), t in zip(DIRECT, measure([s for _, s in DIRECT], setup, N)):
print(f" {label:<22} {t * 1e9:7.1f} нс")
M = 2_000
print(" sorted по 200 элементам (по", f"{M:,}", "сортировок):")
SORTED = [
("key=lambda", "sorted(DATA, key=anon)"),
("key=def", "sorted(DATA, key=named)"),
("key=itemgetter(1)", "sorted(DATA, key=getter)"),
# `sorted(DATA)` без key сюда НЕ добавлен намеренно: он сравнивает
# кортежи по первому элементу, то есть по строке, а три строки выше
# сортируют по числу. Это разная работа, и приписывать разницу способу
# извлечения ключа было бы подлогом. Сравниваются три способа достать
# ОДИН И ТОТ ЖЕ ключ.
]
for (label, _), t in zip(SORTED, measure([s for _, s in SORTED], setup, M)):
print(f" {label:<22} {t * 1e6:7.2f} мкс")