exec中的列表理解与空本地:NameError

Ily*_*rov 13 python closures scope list-comprehension python-3.x

请考虑以下代码段:

def bar():
    return 1
print([bar() for _ in range(5)])
Run Code Online (Sandbox Code Playgroud)

它给出了预期的输出[1, 1, 1, 1, 1].

但是,如果我exec在空环境中尝试使用相同的片段(locals并且globals两者都设置为{}),它会给出NameError:

if 'bar' in globals() or 'bar' in locals():
    del bar
# make sure we reset settings

exec("""
def bar():
    return 1
print([bar() for _ in range(5)])
""", {}, {})

NameError: name 'bar' is not defined
Run Code Online (Sandbox Code Playgroud)

如果我祈求execexec(…, {})或者exec(…),按预期执行.

为什么?

编辑:

还要考虑以下代码段:

def foo():
    def bar():
        return 1
    print('bar' in globals()) # False
    print('bar' in locals()) # True
    print(['bar' in locals() for _ in [1]]) # [False]
    print([bar() for _ in [1, 2]]) # [1, 1]
Run Code Online (Sandbox Code Playgroud)

就像在我的第一个执行官中一样,我们在列表理解中没有本地人的栏.但是,如果我们尝试调用它,它就可以了!

Hen*_*ait 12

您的问题的解决方案在于:

在所有情况下,如果省略可选部分,则代码在当前范围内执行.如果只提供全局变量,则它必须是字典,它将用于全局变量和局部变量.如果给出全局变量和局部变量,则它们分别用于全局变量和局部变量.如果提供,则locals可以是任何映射对象.请记住,在模块级别,全局变量和本地变量是相同的字典.如果exec获得两个单独的对象作为全局变量和局部变量,则代码将被执行,就像它嵌入在类定义中一样.

https://docs.python.org/3/library/functions.html#exec

基本上,你的问题是bar是在范围内定义的locals,仅在locals.因此,此exec()声明有效:

exec("""
def bar():
    return 1
print(bar())
""", {}, {})
Run Code Online (Sandbox Code Playgroud)

但是,列表推导会创建一个新的本地范围,其中一个范围bar未定义,因此无法查找.

此行为可以通过以下方式说明:

exec("""
def bar():
    return 1
print(bar())
print(locals())
print([locals() for _ in range(1)])
""", {}, {})
Run Code Online (Sandbox Code Playgroud)

返回

1
{'bar': <function bar at 0x108efde18>}
[{'_': 0, '.0': <range_iterator object at 0x108fa8780>}]
Run Code Online (Sandbox Code Playgroud)

编辑

在您的原始示例中,定义bar位于(模块级别)全局范围中.这对应于

请记住,在模块级别,全局变量和本地变量是相同的字典.

在该exec示例中,您通过传递两个不同的字典在全局变量和本地变量之间引入了一个人工分割.如果你通过了相同的一个或只有一个全局(这反过来意味着,这将是一个同时用于globalslocals),你的榜样也将工作.

至于编辑中引入的示例,这归结为python中的作用域规则.有关详细说明,请阅读:https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces

简而言之,虽然bar不在列表理解的局部范围内,也不在全局范围内,但它属于foo的范围.并且考虑到Python作用域规则,如果在本地作用域中找不到变量,则将在封闭作用域中搜索它,直到达到全局作用域.在您的示例中,foo的范围位于本地范围和全局范围之间,因此在到达搜索结束之前将找到bar.

然而,这仍然与exec示例不同,其中您传入的本地范围不包含列表推导的范围,而是完全与它分开.

关于范围规则(包括插图)的另一个很好的解释可以在这里找到:http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html


Ant*_*ala 5

由于亨德里克Makait发现,exec文件说,

如果exec得到两个不同的对象为globalslocals,就好像它是嵌入在一个类定义的代码会被执行.

您可以通过将代码嵌入到类定义中来获得相同的行为:

class Foo:
    def bar():
        return 1
    print([bar() for _ in range(5)])
Run Code Online (Sandbox Code Playgroud)

在Python 3中运行它,你就会得到它

Traceback (most recent call last):
  File "foo.py", line 9, in <module>
    class Foo:
  File "foo.py", line 15, in Foo
    print({bar() for _ in range(5)})
  File "foo.py", line 15, in <setcomp>
    print({bar() for _ in range(5)})
NameError: global name 'bar' is not defined
Run Code Online (Sandbox Code Playgroud)

出现错误的原因是Hendrik说为列表推导创建了一个新的隐式局部范围.但是,Python只会在2个范围内查找名称:全局或本地.由于全局范围和新本地范围都不包含名称bar,因此您可以获得NameError.

该代码适用于Python 2,因为列表推导在Python 2中存在一个错误,因为它们不会创建新的作用域,因此它们会将变量泄漏到当前的本地作用域中:

class Foo:
    [1 for a in range(5)]
    print(locals()['a'])
Run Code Online (Sandbox Code Playgroud)

在Python 2中运行它,输出是4.变量a现在位于类体中的局部变量内,并保留上次迭代的值.在Python 3中,您将获得一个KeyError.

如果使用生成器表达式或字典/集合理解,您也可以在Python 2中获得相同的错误:

class Foo:
    def bar():
        return 1
    print({bar() for _ in range(5)})
Run Code Online (Sandbox Code Playgroud)

简单地使用也可以产生错误

class Foo: 
    bar = 42
    class Bar:
        print(bar)
Run Code Online (Sandbox Code Playgroud)

这与此不同

def foo():
    bar = 42
    def baz():
        print(bar)
    baz()
Run Code Online (Sandbox Code Playgroud)

因为执行时foo,Python会baz生成一个闭包,它将bar通过一个特殊的字节码指令访问变量.