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

时间:2017-07-16 19:39:50

标签: python python-3.x scope closures list-comprehension

请考虑以下代码段:

def bar():
    return 1
print([bar() for _ in range(5)])

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

但是,如果我尝试exec空环境中的相同代码段(localsglobals都设置为{}),则会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

如果我调用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]

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

5 个答案:

答案 0 :(得分:11)

问题的解决方案在于:

  

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

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

基本上,您的问题是条形在locals范围内定义,且仅在locals中定义。因此,此exec()语句有效:

exec("""
def bar():
    return 1
print(bar())
""", {}, {})

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

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

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

返回

1
{'bar': <function bar at 0x108efde18>}
[{'_': 0, '.0': <range_iterator object at 0x108fa8780>}]

修改

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

  

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

exec示例中,通过传递两个不同的词典,在全局变量和本地变量之间引入人工分割。如果您传递相同的一个或仅传递全局一个(这反过来意味着这个将用于globalslocals),您的示例也将起作用。

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

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

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

可以在此处找到包括插图在内的范围规则的另一个重要解释:http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html

答案 1 :(得分:5)

Hendrik Makait发现了the exec documentation says that

  

如果exec获得两个单独的对象globalslocals,则代码将被执行,就像它嵌入在类定义中一样。

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

class Foo:
    def bar():
        return 1
    print([bar() for _ in range(5)])

在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

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

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

class Foo:
    [1 for a in range(5)]
    print(locals()['a'])

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

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

class Foo:
    def bar():
        return 1
    print({bar() for _ in range(5)})

只需使用

即可产生错误
class Foo: 
    bar = 42
    class Bar:
        print(bar)

这与

不同
def foo():
    bar = 42
    def baz():
        print(bar)
    baz()

因为在执行foo时,Python将baz放入闭包中,闭包将通过特殊的字节码指令访问bar变量。

答案 2 :(得分:4)

我在这里参加派对已经很晚了,但execution model中有更好的文档参考。

4.2.2 Resolution of names部分中:

  

exec()eval()的类定义块和参数在名称解析的上下文中是特殊的。 ......

然后在4.2.4 Interaction with dynamic features

  

eval()exec()函数无法访问用于解析名称的完整环境。可以在调用者的本地和全局名称空间中解析名称。自由变量不在最近的封闭命名空间中解析,而是在全局命名空间中解析。 [1] exec()eval()函数具有可选参数来覆盖全局和本地命名空间。如果只指定了一个名称空间,则它将用于两者。

[1]出现此限制是因为在编译模块时,这些操作执行的代码不可用。

答案 3 :(得分:1)

  

修改

要回答您编辑的问题,用户@Hendrik Makait说bar不在列表理解的范围内:

def foo():
    def bar():
        return 1
    print('bar' in globals()) # False, because the scope of foo and bar are diferents, foo is globals() scope, bar are in the scope of foo
    print('bar' in locals()) # True
    print(['bar' in locals() for _ in [1]]) # [False], because a new implicit scope is defined in list comprehension, as user @Antti Haapala said
    print([bar() for _ in [1, 2]]) # [1, 1]

回答原始问题:

如果您创建两个不同的词典,它不会识别本地和全局定义,变量不会更新,因为@PM 2Ring说:

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

打印:

1
False #not in globals
True
Traceback (most recent call last):
  File "python", line 17, in <module>
  File "<string>", line 7, in <module>
  File "<string>", line 7, in <listcomp>
NameError: name 'bar' is not defined

一种方法,就是更新变量,比如这个globals()。update(locals()):

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

给出:

True
True
[1, 1, 1, 1, 1]

但是,如果删除字典,或者创建一个字典并将其作为相同的参数提供给exec函数,它就可以工作:

d={}

exec("""
def bar():
    return 1
print("bar" in globals())
print("bar" in locals())
print([bar() for _ in range(5)])
""",d,d)

打印:

True
True
[1, 1, 1, 1, 1]

这就是为什么你得到错误,它无法在全局中找到你的函数

或者简单地说,不要给出参数:

exec("""
def bar():
    return 1
print(bar())
print("bar" in globals())
print("bar" in locals())
print([bar() for _ in range(5)])
""")

导致同样的效果。

答案 4 :(得分:1)

这是一个解决方案!

我们需要在exec()之后获取本地命名空间以跟踪修改。这不仅仅适用于一个命名空间,所以我们这样做了:

 class MagickNameSpace(UserDict, dict):
    """A magic namespace for Python 3 exec().
    We need separate global and local namespaces in exec(). This does not
    work well in Python 3, because in Python 3 the enclosing namespaces are
    not used to look up variables, which seems to be an optimization thing
    as the exec'd code isn't available at module compilation.

    So we make a MagickNameSpace that stores all new variables in a
    separate dict, conforming to the local/enclosing namespace, but
    looks up variables in both.
    """


    def __init__(self, ns, *args, **kw):
        UserDict.__init__(self, *args, **kw)
        self.globals = ns

    def __getitem__(self, key):
        try:
            return self.data[key]
        except KeyError:
            return self.globals[key]

    def __contains__(self, key):
        return key in self.data or key in self.globals

替换旧代码:

exec(code, global_ns, local_ns)
return local_ns

使用:

ns = MagickNameSpace(global_ns)
ns.update(local_ns)
exec(code, ns)
return ns.data