list comprehension in exec with empty locals: NameError

Viewed 2143

Consider the following snippet:

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

It gives an expected output [1, 1, 1, 1, 1].

However, if I try to exec the same snippet in empty environment (locals and globals both are set to {}), it gives 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

If I invoke exec like exec(…, {}) or exec(…), it is executed as expected.

Why?

EDIT:

Consider also the following snippet:

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]

Just like in my first exec, we don't have bar in locals inside list comprehension. However, if we try to invoke it, it works!

5 Answers

Here's a solution!

We needed to get the local namespace out after the exec() to track modifications. This doesn't work with only one namespace, so we did this:

 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

Replace the old code:

exec(code, global_ns, local_ns)
return local_ns

with:

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