如果嵌套在函数中,listcomp无法访问由exec调用的代码中定义的本地

时间:2015-10-01 19:01:56

标签: python list-comprehension python-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的错误吗?

编辑:用python 3.4.0和python 3.4.3

试试

3 个答案:

答案 0 :(得分:8)

问题是因为exec()中的列表理解是无关闭的。

当你在exec()之外创建一个函数(在这种情况下是一个列表推导)时,解析器使用自由变量构建一个元组(代码块使用但不是由它定义的变量,即。在你的情况下g)。这个元组称为函数的闭包。它保存在函数的__closure__成员中。

exec()中,解析器不会在列表解析上构建闭包,而是默认尝试查看globals()字典。这就是为什么在代码开头添加global g会起作用的原因(以及globals().update(locals()))。

在其两个参数版本中使用exec()也将解决问题:Python会将globals()和locals()字典合并为一个(根据the documentation)。执行赋值时,它同时在globals 本地中完成。由于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_TUPLEg中生成的元组中使用LOAD_CLOSURE加载变量MAKE_CLOSUREMAKE_FUNCTION语句创建一个函数,就像之前看到的exec()一样,但是有一个闭包。

我在这里猜测的原因是:第一次读取模块时需要时创建闭包。执行exec()时,无法实现其执行代码中定义的函数需要关闭。对于他来说,其字符串中不以缩进开头的代码位于全局范围内。知道他是否以需要关闭的方式调用的唯一方法是需要delete from classroom c left join lesson_plan_classroom lpc on c.id = lpc.classroom_id left join response r on c.id = r.classroom_id where r.classroom_id is null and lpc.classroom_id is null and c.enabled = 0; 来检查当前范围(这对我来说似乎很狡猾)。

这确实是一种模糊不清的行为,可以解释但当它发生时肯定会引起一些人的注意。这是the Python guide中很好解释的副作用,但很难理解为什么它适用于这种特殊情况。

我的所有分析都是在Python 3上进行的,我没有在Python 2上做过任何尝试。

答案 1 :(得分: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()字典。这与优化的查找混淆。见,例如, this question and its answers

特别是Python doc explains

  

不应修改此词典的内容;更改可能不会影响解释器使用的本地和自由变量的值。

因为您在某个函数中调用了exec()并且没有明确传入locals(),所以您修改了函数的本地文件,并且正如文档所解释的那样,总是有效。

正如其他人所指出的那样,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,它将用于全局访问和本地访问(这与你的代码没有在函数或类定义中执行的情况完全相同 - 是< / em>没有locals())。因此,在这种情况下,与locals()相关的错误不会出现。

上面的类示例是:

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

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

print(Foo.x, Foo.g)

答案 2 :(得分:-2)

确定!有人看了一下,看起来您的行x = [g for i in range(5)]正在尝试创建一个新的未初始化的值g,而不是使用您之前定义的值。

pythonic修复是将您的范围传递到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)