python - 如果嵌套在函数中,listcomp 无法访问 exec 调用的代码中定义的局部变量

标签 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)

我知道它与局部变量和全局变量有关,就好像我从我的主作用域向 exec 函数传递局部变量和全局变量一样,它工作正常,但我不完全明白发生了什么。

这可能是 Cython 的错误吗?

编辑:用 python 3.4.0 和 python 3.4.3 试过了

最佳答案

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

当您在 exec() 之外创建函数(在本例中为列表理解)时,解析器会构建一个包含自由变量(代码块使用但 undefined variable )的元组通过它,即 g 在你的情况下)。这个元组称为函数的闭包。它保存在函数的 __closure__ 成员中。

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

在其两个参数版本中使用 exec() 也可以解决问题:Python 会将 globals() 和 locals() 字典合并为一个字典(根据 the documentation )。执行赋值时,会同时在全局 局部中完成。由于 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_CLOSUREMAKE_CLOSURE 语句创建一个函数,就像前面看到的 MAKE_FUNCTION 一样,但是有一个闭包。

这是我对这种方式的原因的猜测:闭包是在第一次读取模块时在需要时创建的。当 exec() 被执行时,它无法意识到在其执行的代码中定义的函数需要关闭。对他来说,其字符串中不以缩进开头的代码在全局范围内。要知道他是否以需要关闭的方式被调用的唯一方法是需要 exec() 检查当前范围(这对我来说似乎很老套)。

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

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

关于python - 如果嵌套在函数中,listcomp 无法访问 exec 调用的代码中定义的局部变量,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32894942/

相关文章:

python - 如何从字典列表创建稀疏 DataFrame

python - 在整齐的列/表中打印列表列表

python - 使用 exec 导入特定模块的优点和缺点?

python - 为什么要避免 exec() 和 eval()?

python - 在焕然一新的 Python 环境中以编程方式从 Python 内部执行 Python 文件

python - 写入 csv、Python、不同的数据类型

python - 使用 Django,在查询集中的每个对象上添加特定字段的最佳实践方法是什么?

python - PyQt4:更改 QTabBar 上的文本大小和字体

python - 是否可以在 python 中使用 in-line for 中断

python - 带有两个列表的嵌套列表理解