MEASUREMENT
bench/lambda/identity.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/lambda
- How to run it
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v late_binding.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
Замеры для урока «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 — нет, она устойчиво в полтора раза.
Поэтому в уроке сказано «неотличимо» про первую пару и названа кратность для
второй.
Script
51 lines"""Чем lambda отличается от def. Ответ: почти ничем."""
import sys, dis
print("PY", sys.version.split()[0])
def named(x):
return x + 1
anon = lambda x: x + 1 # noqa: E731
print("1) типы:", type(named).__name__, "и", type(anon).__name__)
print(" один и тот же тип:", type(named) is type(anon))
# 2. Байт-код тела совпадает до инструкции.
a = [i.opname for i in dis.get_instructions(named.__code__)]
b = [i.opname for i in dis.get_instructions(anon.__code__)]
print("2) байт-код тела совпадает:", a == b)
print(" ", " ".join(a))
# 3. А вот имя — не совпадает, и это видно в трассировке.
print("3) __name__:", named.__name__, "|", anon.__name__)
print(" __qualname__:", named.__qualname__, "|", anon.__qualname__)
def boom_named(x):
return int(x)
boom_anon = lambda x: int(x) # noqa: E731
import traceback
for fn, label in ((boom_named, "def"), (boom_anon, "lambda")):
try:
fn("тест")
except ValueError:
# Берётся последний кадр — тот, где ошибка возникла. Имя функции в нём
# и есть разница между def и lambda: у второй его нет.
line = [l for l in traceback.format_exc().splitlines() if ", in " in l][-1]
print(f" последний кадр ({label}):", line.strip().split(", in ")[-1])
# 4. Чего lambda не умеет: в ней только ВЫРАЖЕНИЕ.
for src, what in [
("lambda x: (x := 1)", "морж — можно, это выражение"),
("lambda x: x = 1", "присваивание"),
("lambda x: return x", "return"),
("lambda x: raise ValueError", "raise"),
("lambda x: assert x", "assert"),
("lambda x -> int: x", "аннотация возврата"),
]:
try:
compile(src, "<t>", "eval")
print(f"4) {what:<28} — компилируется")
except SyntaxError:
print(f"4) {what:<28} — SyntaxError")