Listcomp не может получить доступ к локальным файлам, определенным в коде, вызываемом exec, если он вложен в функцию

Есть ли у гуру python возможность объяснить, почему этот код не работает:

def f(code_str):
    exec(code_str)

code = """
g = 5
x = [g for i in range(5)]
"""

f(code)

Ошибка:

Traceback (most recent call last):
  File "py_exec_test.py", line 9, in <module>
    f(code)
  File "py_exec_test.py", line 2, in f
    exec(code_str)
  File "<string>", line 3, in <module>
  File "<string>", line 3, in <listcomp>
NameError: name 'g' is not defined

в то время как этот работает отлично:

code = """
g = 5
x = [g for i in range(5)]
"""

exec(code)

Я знаю, что это имеет какое-то отношение к locals и globals, как если бы я передавал функцию exec locals и globals из моей основной области, она отлично работает, но я не совсем понимаю, что происходит.

Может быть, это ошибка с Cython?

EDIT: пробовал это с помощью python 3.4.0 и python 3.4.3

Ответ 1

Проблема заключается в том, что в exec() отсутствует понимание списка.

Когда вы создаете функцию (в данном случае, для понимания списка) вне exec(), синтаксический анализатор строит кортеж со свободными переменными (переменными, используемыми блоком кода, но не определяемыми им, т.е. g в вашем случае). Этот кортеж называется закрытием функции. Он хранится в члене __closure__ функции.

Когда в exec(), синтаксический анализатор не будет строить замыкание в понимании списка и вместо этого пытается по умолчанию заглянуть в словарь globals(). Поэтому добавление global g в начале кода будет работать (а также globals().update(locals())).

Использование exec() в двух версиях параметров также решит проблему: Python объединит словарь globals() и locals() в одном (согласно документации). Когда выполняется переадресация, оно выполняется одновременно в глобальных и локальных сетях. Поскольку Python будет проверять глобальные переменные, этот подход будет работать.

Вот еще один взгляд на проблему:

import dis

code = """
g = 5
x = [g for i in range(5)]
"""

a = compile(code, '<test_module>', 'exec')
dis.dis(a)
print("###")
dis.dis(a.co_consts[1])

Этот код создает этот байт-код:

  2           0 LOAD_CONST               0 (5)
              3 STORE_NAME               0 (g)

  3           6 LOAD_CONST               1 (<code object <listcomp> at 0x7fb1b22ceb70, file "<boum>", line 3>)
              9 LOAD_CONST               2 ('<listcomp>')
             12 MAKE_FUNCTION            0
             15 LOAD_NAME                1 (range)
             18 LOAD_CONST               0 (5)
             21 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             24 GET_ITER
             25 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             28 STORE_NAME               2 (x)
             31 LOAD_CONST               3 (None)
             34 RETURN_VALUE
###
  3           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                12 (to 21)
              9 STORE_FAST               1 (i)
             12 LOAD_GLOBAL              0 (g)      <---- THIS LINE
             15 LIST_APPEND              2
             18 JUMP_ABSOLUTE            6
        >>   21 RETURN_VALUE

Обратите внимание, как он выполняет LOAD_GLOBAL для загрузки g в конце.

Теперь, если у вас есть этот код:

def Foo():
    a = compile(code, '<boum>', 'exec')
    dis.dis(a)
    print("###")
    dis.dis(a.co_consts[1])
    exec(code)

Foo()

Это обеспечит точно такой же байт-код, что проблематично: поскольку мы находимся в функции, g не будет объявлен в глобальной переменной, а в локалях этой функции. Но Python пытается найти его в глобальных переменных (с помощью LOAD_GLOBAL)!

Это то, что делает интерпретатор вне exec():

def Bar():
    g = 5
    x = [g for i in range(5)]

dis.dis(Bar)
print("###")
dis.dis(Bar.__code__.co_consts[2])

Этот код дает нам этот байт-код:

30           0 LOAD_CONST               1 (5)
             3 STORE_DEREF              0 (g)

31           6 LOAD_CLOSURE             0 (g)
              9 BUILD_TUPLE              1
             12 LOAD_CONST               2 (<code object <listcomp> at 0x7fb1b22ae030, file "test.py", line 31>)
             15 LOAD_CONST               3 ('Bar.<locals>.<listcomp>')
             18 MAKE_CLOSURE             0
             21 LOAD_GLOBAL              0 (range)
             24 LOAD_CONST               1 (5)
             27 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             30 GET_ITER
             31 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             34 STORE_FAST               0 (x)
             37 LOAD_CONST               0 (None)
             40 RETURN_VALUE
###
 31           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                12 (to 21)
              9 STORE_FAST               1 (i)
             12 LOAD_DEREF               0 (g)      <---- THIS LINE
             15 LIST_APPEND              2
             18 JUMP_ABSOLUTE            6
        >>   21 RETURN_VALUE

Как вы можете видеть, g загружается с помощью LOAD_DEREF, доступного в кортеже, сгенерированном в BUILD_TUPLE, который загрузил переменную g с помощью LOAD_CLOSURE. Оператор MAKE_CLOSURE создает функцию, как и раньше, MAKE_FUNCTION, но с закрытием.

Здесь я догадываюсь, почему это так: закрытие создается, когда это необходимо, когда модуль читается в первый раз. Когда exec() выполняется, он не может реализовать функции, определенные в его исполняемом коде, необходимо закрыть. Для него код в своей строке, который не начинается с отступов, находится в глобальной области. Единственный способ узнать, был ли он вызван таким образом, который требует закрытия, потребовал бы exec() для проверки текущей области (что кажется мне довольно хакерским).

Это действительно неясное поведение, которое можно объяснить, но, конечно же, вызывает некоторые брови, когда это происходит. Это побочный эффект, хорошо объясненный в руководстве Python, хотя трудно понять, почему оно относится к этому конкретному случаю.

Весь мой анализ был сделан на Python 3, я ничего не пробовал на Python 2.

Ответ 2

РЕДАКТИРОВАТЬ 2

Как заметили другие комментаторы, вы обнаружили ошибку в Python 3 (для меня это не происходит в версии 2.7).

Как обсуждалось в комментариях ниже этого ответа, исходный код:

def f(code_str):
    exec(code_str)

функционально эквивалентно:

def f(code_str):
    exec(code_str, globals(), locals())

На моей машине, работающей на 3.4, она функционально эквивалентна степени, в которой она будет взрываться одинаково. Ошибка здесь связана с запуском понимания списка при наличии двух объектов сопоставления. Например:

def f(code_str):
    exec(code_str, globals(), {})

также завершится с тем же исключением.

Чтобы избежать провокации этой ошибки, вам нужно передать ровно один объект сопоставления (потому что не передавать какой-либо эквивалент для прохождения двух), и чтобы гарантировать, что он работает во всех случаях, вы никогда не должны передавать функцию locals() как таковую объект сопоставления.

Остальная часть этого ответа была написана до того, как я понял, что поведение отличается от 3. Я оставляю это, потому что это все еще хороший совет и дает некоторое представление о поведении exec.

Вы никогда не должны напрямую изменять словарь функций locals(). Это связано с оптимизированным поиском. См., Например, этот вопрос и его ответы

В частности, как объясняет Python doc:

Содержимое этого словаря не должно изменяться; изменения могут не влиять на значения локальных и свободных переменных, используемых интерпретатором.

Поскольку вы вызвали exec() изнутри функции и явно не прошли через locals(), вы изменили локальные функции функции, и, как объясняет Doc, это не всегда работает.

Таким образом, Pythonic-путь, как указывали другие, заключается в явной передаче объектов отображения в exec().

Python 2.7

Когда это ОК, чтобы изменить locals()? Один ответ - это когда вы строите класс - в этот момент это просто другой словарь:

code = """
g = 5
x = [g for i in range(5)]
"""

class Foo(object):
    exec(code)

print Foo.x, Foo.g

[5, 5, 5, 5, 5] 5

РЕДАКТИРОВАТЬ - Python 3 Как отмечают другие, здесь существует ошибка с locals(), независимо от того, находитесь ли вы внутри функции. Вы можете обойти это, передав только один параметр для глобальных переменных. Документация Python объясняет, что если вы передаете только один dict, который будет использоваться как для глобального, так и для локального доступа (это действительно то же самое, что если ваш код не выполняется в определении функции или класса, нет locals()). Таким образом, ошибка, связанная с locals(), не появляется в этом случае.

Пример класса выше:

code = """
g = 5
x = [g for i in range(5)]
"""

class Foo(object):
    exec(code, vars())

print(Foo.x, Foo.g)

Ответ 3

Ok! Огляделся ли, и похоже, что ваша строка x = [g for i in range(5)] пытается создать новое и неинициализированное значение g, а не использовать тот, который вы определили ранее.

Pythonic fix должен передать вашу область в ваш exec() следующим образом:

def f(code,globals,locals):
    exec(code,globals,locals)

code = """
g = 5
x = [g for i in range(5)]
print(x)
"""

f(code,globals(),locals())

Это был очень хороший вопрос. Я многому научился, отвечая на него.

Ссылка на это для получения дополнительной информации по exec(): https://docs.python.org/3/library/functions.html#exec

Сокращенная версия была предложена @Pynchia и определяет globals() при вызове exec() внутри функции.

def f(code):
    exec(code,globals())

code = """
g = 5
x = [g for i in range(5)]
print(x)
"""

f(code)