Search code examples
pythongeneratorexec

exec inside a function and generator


I need to write a custom exec function in python (for several purposes but this is not the problem here, so this custom exec which is called myExec will do exactly as exec for now).

I went into this problem :

def myExec(code):
    exec(code)

code = """
a = 1
print(a)
u = [a for x in range(3)]
print(u)
"""

myExec(code)

Running this program gives

1
Traceback (most recent call last):
  File "___.py", line 12, in <module>
    myExec(code)
  File "___.py", line 2, in myExec
    exec(code, globals(), locals())
  File "<string>", line 4, in <module>
  File "<string>", line 4, in <listcomp>
NameError: name 'a' is not defined

So print(a) went without any problems. But the error occurs with the line u = [a for x in range(3)]. When the generator object is converted into a list, the name a seems undefined.

Note that if the line were u = [a, a, a], then, no error is raised. Nor if we use exec instead of myExec.

Any reason why and how to solve this ?


Solution

  • We can explain this behavior if we take a look at the decompiled code.

    from dis import dis
    
    def myExec(code):
        dis(code)
    
    • a = 1 is compiled to STORE_NAME, so it stores a as a local variable here
    • print(a) uses LOAD_NAME to load the local a. It is a local variable, so LOAD_NAME finds it.
    • the list comprehension is compiled into a function that uses LOAD_GLOBAL for a

    That's where the error is coming from. a was created as a local variable and is accessed as a global one in the list comprehension. This results in a name error.

    This also explains why the exec works in the global scope (either by calling exec in the global scope or by passing globals()). Because then STORE_NAME stores a in the current scope (which is global) and LOAD_GLOBAL can find a.

    If you switch to Python3.12 which implements PEP 709 – Inlined comprehensions you will see that no extra function is created for the list comprehension and a is looked up with LOAD_NAME and can be found.

    So to fix your issue: either upgrade to Python3.12 or pass the globals()