ЗАМЕР
bench/lambda/late_binding.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 — нет, она устойчиво в полтора раза.
Поэтому в уроке сказано «неотличимо» про первую пару и названа кратность для
второй.
Скрипт
50 строк"""Ошибка, которая не падает: замыкание в цикле."""
import sys
print("PY", sys.version.split()[0])
# 1. Три функции, каждая «своя» — но все возвращают одно и то же.
fns = [lambda: i for i in range(3)]
print("1) [lambda: i for i in range(3)] ->", [f() for f in fns])
print(" ожидали [0, 1, 2]")
# 2. Причина: замыкается ПЕРЕМЕННАЯ, а не значение. У всех трёх одна ячейка.
cells = {id(f.__closure__[0]) for f in fns}
print("2) разных ячеек замыкания:", len(cells), "— то есть одна на всех")
print(" значение в ячейке:", fns[0].__closure__[0].cell_contents)
# 3. Обход: значение фиксируется аргументом по умолчанию — он вычисляется
# в момент СОЗДАНИЯ функции, а не вызова.
fixed = [lambda i=i: i for i in range(3)]
print("3) [lambda i=i: i ...] ->", [f() for f in fixed])
print(" замыканий у такой функции:", fixed[0].__closure__)
# 4. То же самое случается и с обычным def — дело не в lambda.
defs = []
for i in range(3):
def make():
return i
defs.append(make)
print("4) то же с def ->", [f() for f in defs])
# 5. А вот у functools.partial проблемы нет: аргумент вычисляется сразу.
from functools import partial
parts = [partial(lambda x: x, i) for i in range(3)]
print("5) partial ->", [f() for f in parts])
# 6. ВАЖНАЯ РАЗНИЦА, которой нет в FAQ: включение и цикл ошибаются по-разному.
#
# В FAQ пример на for-цикле, и там переменная УТЕКАЕТ наружу: после цикла
# её видно и можно поменять, и результат функций изменится следом. У
# включения своя область видимости — переменная наружу не выходит, но
# ячейка у трёх функций всё равно одна.
leaked = []
for x in range(3):
leaked.append(lambda: x)
print("6) после for-цикла x виден снаружи:", "x" in dir(), "| значение:", x)
x = 100
print(" поменяли x -> функции вернули:", [f() for f in leaked])
comp = [lambda: y for y in range(3)]
print(" после включения y виден снаружи:", "y" in dir())
print(" а результат тот же:", [f() for f in comp])