Есть ли у лексической сферы динамический аспект?
Кажется, это обычное явление, когда доступ к лексической области может быть осуществлен во время компиляции (или статическим анализатором, так как мой пример на Python), основанным просто на расположении в исходном коде.
Вот очень простой пример, когда одна функция имеет два замыкания с разными значениями для a
,
def elvis(a):
def f(s):
return a + ' for the ' + s
return f
f1 = elvis('one')
f2 = elvis('two')
print f1('money'), f2('show')
У меня нет проблем с идеей, что, когда мы читаем код для функции f
, когда мы видим a
не определено в f
, так что мы всплываем к включающей функции и находим там, и это то, что a
в f
относится к. Расположение в исходном коде достаточно, чтобы сказать мне, что f
получает значение для a
из ограждающей области.
Но, как описано здесь, когда вызывается функция, ее локальный фрейм расширяет родительскую среду. Так что поиск окружения во время выполнения не проблема. Но в чем я не уверен, так это в том, что статический анализатор всегда может определить, на какое закрытие ссылаются во время компиляции, еще до того, как код будет запущен. В приведенном выше примере очевидно, что elvis
имеет два замыкания, и их легко отслеживать, но в других случаях все будет не так просто. Интуитивно я нервничаю, что попытка статического анализа может привести к общей проблеме остановки.
Итак, действительно ли лексическая область видимости имеет динамический аспект, когда местоположение в исходном коде говорит нам, что включенная область действия включена, но не обязательно, какое закрытие упоминается? Или это решаемая проблема в компиляторах, и все ссылки внутри функций на их замыкания действительно могут быть детально проработаны статически?
Или ответ зависит от языка программирования - в этом случае лексическая область видимости не так сильна, как я думал?
[EDIT @comments:
В терминах моего примера я могу перефразировать свой вопрос: я читаю утверждения типа "Лексическое разрешение можно определить во время компиляции", но все же удивлялся, как ссылки на значение a
в f1
а также f2
может быть разработан статически / во время компиляции (в целом).
Решение проблемы заключается в том, что лексическая область видимости не требует больших затрат. LS может сказать нам, во время компиляции, что- то называется a
будет определяться всякий раз, когда я нахожусь в f
(и это, очевидно, может быть разработано статически; это определение лексического контекста), но определение того, какое значение оно на самом деле принимает (или какое замыкание активно): 1) выходит за рамки концепции LS, 2) выполняется во время выполнения (не статично).) в некотором смысле динамичен, но, конечно, 3) использует правило, отличное от динамического обзора.
Выводное сообщение, цитируемое @PatrickMaupin, гласит: "Нужно проделать некоторую динамическую работу". ]
3 ответа
Замыкания могут быть реализованы несколькими способами. Одним из них является на самом деле захватить среду... другими словами, рассмотрим пример
def foo(x):
y = 1
z = 2
def bar(a):
return (x, y, a)
return bar
Решение захвата env выглядит так:
foo
вводится и строится локальный фрейм, содержащийx
,y
,z
,bar
имена. Имяx
привязан к параметру, имениy
а такжеz
на 1 и 2, имяbar
на закрытие- закрытие назначено
bar
на самом деле захватывает весь родительский кадр, поэтому, когда он вызывается, он может искать имяa
в своем собственном локальном кадре и может искатьx
а такжеy
вместо этого в захваченном родительском кадре.
При таком подходе (это не тот подход, который используется в Python) переменная z
будет оставаться в живых до тех пор, пока замыкание остается живым, даже если оно не ссылается на замыкание.
Другой вариант, немного более сложный для реализации работ, например:
- во время компиляции код анализируется и назначается закрытие
bar
обнаружен захват именx
а такжеy
из текущей области. - поэтому эти две переменные классифицируются как "ячейки", и они выделяются отдельно от локальной структуры
- замыкание хранит адрес этих переменных, и каждый доступ к ним требует двойной косвенности (ячейка - указатель на то, где значение действительно хранится)
Это требует дополнительных затрат времени при создании замыкания, поскольку каждая отдельная захваченная ячейка требует копирования внутри объекта замыкания (вместо простого копирования указателя на родительский кадр), но имеет то преимущество, что не захватывает весь кадр, так что пример z
не останется в живых после foo
только возврат x
а также y
будут.
Это то, что делает Python... в основном во время компиляции, когда закрытие (или именованная функция или lambda
) найдена подкомпиляция. Во время компиляции, когда есть поиск, который разрешает родительскую функцию, переменная помечается как ячейка.
Немного раздражает то, что при захвате параметра (как в foo
пример) также необходимо выполнить дополнительную операцию копирования в прологе для преобразования переданного значения в ячейку. Это в Python не видно в байт-коде, но выполняется напрямую механизмом вызова.
Еще одна неприятность заключается в том, что каждый доступ к захваченной переменной требует двойной косвенности даже в родительском контексте.
Преимущество состоит в том, что замыкания захватывают только действительно ссылочные переменные, и когда они не захватывают какой-либо сгенерированный код, он эффективен как обычная функция.
Чтобы увидеть, как это работает в Python, вы можете использовать dis
Модуль для проверки сгенерированного байт-кода:
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (1)
3 STORE_DEREF 1 (y)
3 6 LOAD_CONST 2 (2)
9 STORE_FAST 1 (z)
4 12 LOAD_CLOSURE 0 (x)
15 LOAD_CLOSURE 1 (y)
18 BUILD_TUPLE 2
21 LOAD_CONST 3 (<code object bar at 0x7f6ff6582270, file "<stdin>", line 4>)
24 LOAD_CONST 4 ('foo.<locals>.bar')
27 MAKE_CLOSURE 0
30 STORE_FAST 2 (bar)
6 33 LOAD_FAST 2 (bar)
36 RETURN_VALUE
>>>
как вы можете видеть сгенерированные хранилища кода 1
в y
с STORE_DEREF
(операция, которая пишет в ячейку, таким образом, используя двойное косвенное обращение) и вместо этого сохраняет 2
в z
используя STORE_FAST
(z
не был захвачен и является просто локальным в текущем кадре). Когда код foo
начинается исполнение x
был уже завернут в камеру с помощью механизма вызова.
bar
это просто локальная переменная, так STORE_FAST
используется для записи в него, но для создания замыкания x
а также y
нужно копировать по отдельности (они помещаются в кортеж перед вызовом MAKE_CLOSURE
опкод).
Код самого замыкания виден с:
>>> dis.dis(foo(12))
5 0 LOAD_DEREF 0 (x)
3 LOAD_DEREF 1 (y)
6 LOAD_FAST 0 (a)
9 BUILD_TUPLE 3
12 RETURN_VALUE
и вы можете увидеть, что внутри возвращенного закрытия x
а также y
доступны с LOAD_DEREF
, Независимо от того, на скольких уровнях "вверх" в иерархии вложенных функций определена переменная, на самом деле это двойное косвенное отклонение, потому что цена выплачивается при построении замыкания. Закрытые переменные доступны лишь немного медленнее (по постоянному коэффициенту) по отношению к местным жителям... не нужно проходить "цепочку областей действия" во время выполнения.
Компиляторы, которые являются еще более сложными, например, SBCL (оптимизирующий компилятор для Common Lisp, генерирующий собственный код), также выполняют "escape-анализ", чтобы определить, действительно ли замыкание может выжить во вмещающей функции. Когда этого не происходит (т.е. если bar
используется только внутри foo
и не сохраняются и не возвращаются) ячейки могут быть размещены в стеке, а не в куче, что снижает количество "обработанных" во время выполнения (выделение объектов в куче, которые требуют восстановления сборки мусора).
Это различие в литературе известно как "нисходящий / восходящий фунгарг"; т.е. если захваченные переменные видны только на более низких уровнях (т. е. в замыкании или в более глубоких замыканиях, созданных внутри замыкания) или также на верхних уровнях (т. е. если мой абонент сможет получить доступ к моим захваченным местным жителям).
Для решения восходящей проблемы funarg необходим сборщик мусора, и поэтому замыкания в C++ не предоставляют такой возможности.
Это решенная проблема... в любом случае. Python использует чисто лексическую область видимости, а замыкание определяется статически. Другие языки допускают динамическую область видимости - и замыкание определяется во время выполнения, ища свой путь вверх по стеку вызовов во время выполнения вместо стека разбора.
Это достаточное объяснение?
В Python переменная определяется как локальная, если она когда-либо назначена (появляется в LHS назначения) и не объявлена явно глобальной или нелокальной.
Таким образом, можно разработать цепочку лексических областей видимости, чтобы статически определить, какой идентификатор будет найден в какой функции. Однако некоторую динамическую работу еще предстоит проделать, поскольку вы можете произвольно вкладывать функции, поэтому, если функция A включает функцию B, которая включает функцию C, то для того, чтобы функция C могла получить доступ к переменной из функции A, вы должны найти правильный кадр для А. (То же самое для закрытия.)