MEASUREMENT
bench/comprehensions/scope.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/comprehensions
- How to run it
for v in 3.11 3.12 3.13 3.14; do python$v bench/comprehensions/scope.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
Замеры для урока «Включения: где проходит граница»
| Скрипт | Что показывает |
|---|---|
scope.py |
где именно проходит граница области видимости: переменная не течёт, первый итерируемый вычисляется снаружи, тело класса даёт NameError там, где функция не даёт |
walrus.py |
единственное, что ходит через границу наружу: := связывает имя в объемлющей области (PEP 572), плюс два запрета и заявленный мотив |
silent.py |
две ошибки, которые не падают: подменённая константа в теле класса и утёкший морж |
cost.py |
включение против цикла против map — и что на самом деле решает |
bytecode.py |
что изменил PEP 709, чего не изменил, и почему «вынести метод в локальную» больше не помогает |
for v in 3.11 3.12 3.13 3.14; do python$v bench/comprehensions/scope.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). После перехода на чередование расхождение между прогонами не выходит за несколько процентов. Перезаписывать запись прогона имеет смысл только вместе с проверкой задачи: если после перезапуска ответ изменился, менять нужно задачу, а не файл.
Главное, что здесь измерено
Граница проходит не по скобкам включения. Справочник: «aside from the
iterable expression in the leftmost for clause, the comprehension is executed
in a separate implicitly nested scope». Отсюда четыре строки подряд в одном
теле класса, из которых работают первая и последняя:
[x for x in raw] -> [2, 3, 5]
[x * multiplier for x in raw] -> NameError: name 'multiplier' is not defined
[x for x in raw if x > limit] -> NameError: name 'limit' is not defined
[x + y for x in raw for y in raw] -> NameError: name 'raw' is not defined
обычный for/append по raw -> [20, 30, 50]
Четвёртая строка — самая наглядная: raw в ней написано дважды, и первое
вхождение видно, а второе нет.
Правило не менялось четыре версии подряд. PEP 709 (3.12) встроил включения
в вызывающий код — MAKE_FUNCTION и вложенного объекта кода у них больше нет,
— но область видимости осталась прежней. Проверено: на 3.11, 3.12, 3.13 и 3.14
scope.py печатает одни и те же три NameError.
Выбор «включение или цикл или map» почти ничего не стоит. На 3.13.7 разброс
между тремя способами — 1,13 раза. Решает другое: убрать вызов кода на Python
из тела даёт 1,7 раза, а заменить условие на Python вызовом filter(None, …) —
ещё 1,8.
Приём «вынести метод в локальную» стоит дороже, чем не выносить. Около
десяти процентов, устойчиво. Инструкций в теле цикла при этом поровну — девять
и девять; разница в том, что out.append(...) компилируется в LOAD_ATTR с
установленным младшим битом (форма, которая кладёт на стек метод и self
сразу), а вызов через локальную переменную идёт обычным путём и подставляет
self на каждом вызове.
Чего здесь нет и почему
Сравнения спискового включения с генераторным выражением. Оно разобрано в уроке «Итераторы и генераторы» — там же и про то, что PEP 709 генераторных выражений не касается. Повторять его здесь значило бы дать два разных набора чисел на один и тот же вопрос.
Времени для walrus.py и scope.py. Область видимости — вопрос
семантики; наносекунды в этих разделах ничего бы не подкрепили.
Правило версий
Время между версиями не сравнивается. Числа 3.13.7 и 3.14.7 в cost.py
приводятся рядом как два независимых результата; сравнивать можно только
кратности внутри одного прогона. Наблюдения scope.py, walrus.py и
bytecode.py от версии к версии сравнивать можно: там не время, а имена
инструкций, наличие объектов кода и текст сообщений об ошибках.
Script
119 lines"""Где проходит граница области видимости включения — и что через неё ходит.
ЗАЧЕМ ЭТО. Про включения знают одно: «переменная цикла наружу не течёт». Это
верно и это половина правила. Вторая половина в справочнике сформулирована так:
However, aside from the iterable expression in the leftmost `for` clause,
the comprehension is executed in a separate implicitly nested scope.
То есть граница проходит не по скобкам включения, а внутри него: первый
итерируемый вычисляется СНАРУЖИ, всё остальное — внутри. Из этого следуют три
вещи, которые обычно узнают по ошибке, и все три проверяются здесь запуском.
1. Переменная цикла не течёт наружу.
2. Первый итерируемый видит имена внешней области, остальные части — нет.
3. В теле класса это даёт NameError на имя, которое написано строкой выше.
"""
import sys
print("PY", sys.version.split()[0])
def rule(title):
print()
print(title)
print("-" * len(title))
# ------------------------------------------------- 1. переменная не течёт
rule("1. Переменная цикла наружу не течёт")
y = "значение снаружи"
squares = [y for y in range(3)]
print(f" [y for y in range(3)] -> {squares}")
print(f" y после включения -> {y!r}")
print(" Обычный цикл вёл бы себя иначе:")
for y2 in range(3):
pass
print(f" for y2 in range(3) -> y2 после цикла = {y2!r}")
# ------------------------------------- 2. первый итерируемый — снаружи
rule("2. Первый итерируемый вычисляется снаружи, остальное — внутри")
class Prices:
"""Тело класса — единственное место, где разницу видно без ухищрений.
Функция сюда не годится: у неё есть замыкание, и включение внутри функции
видит её локальные имена нормально. У класса замыкания нет — справочник
говорит об этом прямо: «The scope of names defined in a class block is
limited to the class block; it does not extend to the code blocks of
methods. This includes comprehensions and generator expressions».
"""
raw = [2, 3, 5]
multiplier = 10
limit = 3
# Первый итерируемый — виден.
ok_first = [x for x in raw]
# Имя в выражении — не видно.
try:
bad_expr = [x * multiplier for x in raw]
except NameError as e:
bad_expr = f"NameError: {e}"
# Имя в условии — не видно.
try:
bad_cond = [x for x in raw if x > limit]
except NameError as e:
bad_cond = f"NameError: {e}"
# Второй итерируемый — не виден.
try:
bad_second = [x + y for x in raw for y in raw]
except NameError as e:
bad_second = f"NameError: {e}"
# А обычный цикл в том же теле класса видит всё.
loop = []
for x in raw:
loop.append(x * multiplier)
print(f" [x for x in raw] -> {Prices.ok_first}")
print(f" [x * multiplier for x in raw] -> {Prices.bad_expr}")
print(f" [x for x in raw if x > limit] -> {Prices.bad_cond}")
print(f" [x + y for x in raw for y in raw]-> {Prices.bad_second}")
print(f" тот же цикл через for/append -> {Prices.loop}")
print()
print(" Четыре строки подряд в одном теле класса. Первая работает, потому что")
print(" raw — первый итерируемый. Три следующие падают. Пятая работает,")
print(" потому что обычный цикл никакой отдельной области не заводит.")
# -------------------------------------------- 3. внутри функции всё иначе
rule("3. Внутри функции тех же ошибок нет")
def inside_function():
raw = [2, 3, 5]
multiplier = 10
limit = 3
return (
[x * multiplier for x in raw],
[x for x in raw if x > limit],
[x + y for x in raw for y in raw][:3],
)
expr, cond, second = inside_function()
print(f" [x * multiplier for x in raw] -> {expr}")
print(f" [x for x in raw if x > limit] -> {cond}")
print(f" [x + y for x in raw for y in raw] -> {second} ...")
print()
print(" Ровно тот же код, что падал в теле класса. Разница не во включении,")
print(" а в том, что у функции есть замыкание, а у тела класса нет.")