Search code examples
pythonpython-3.xpython-exec

Python 3 - How to exec a string as if it were substituted directly?


Problem Description

I am curious if it is possible to exec a string within a function as if the string were substituted for exec directly (with appropriate indentation). I understand that in 99.9% of cases, you shouldn't be using exec but I'm more interested in if this can be done rather than if it should be done.

The behavior I want is equivalent to:

GLOBAL_CONSTANT = 1

def test_func():
    def A():
        return GLOBAL_CONSTANT
    def B():
        return A()
    return B

func = test_func()
assert func() == 1

But I am given instead:

GLOBAL_CONSTANT = 1

EXEC_STR = """
def A():
    return GLOBAL_CONSTANT
def B():
    return A()
"""

def exec_and_extract(exec_str, var_name):
    # Insert code here

func = exec_and_extract(EXEC_STR, 'B')
assert func() == 1

Failed Attempts

def exec_and_extract(exec_str, var_name):
    exec(EXEC_STR)  # equivalent to exec(EXEC_STR, globals(), locals())
    return locals()[var_name]

NameError: name 'A' is not defined when calling func() since A and B exist inside exec_and_extract's locals() but the execution context while running A or B is exec_and_extract's globals().


def exec_and_extract(exec_str, var_name):
    exec(EXEC_STR, locals())  # equivalent to exec(EXEC_STR, locals(), locals())
    return locals()[var_name]

NameError: name 'GLOBAL_CONSTANT' is not defined when calling A from inside func() since the execution context of A is exec_and_extract's locals() which does not contain GLOBAL_CONSTANT.


def exec_and_extract(exec_str, var_name):
    exec(EXEC_STR, globals())  # equivalent to exec(EXEC_STR, globals(), globals())
    return globals()[var_name]

Works but pollutes global namespace, not equivalent.


def exec_and_extract(exec_str, var_name):
    locals().update(globals())
    exec(EXEC_STR, locals())  # equivalent to exec(EXEC_STR, locals(), locals())
    return locals()[var_name]

Works but requires copying the entire content of exec_and_extract's globals() into its locals() which is a waste of time if globals() is large (of course not applicable in this contrived example). Additionally, is subtly not the same as the "paste in code" version since if one of the arguments to exec_and_extract happened to be GLOBAL_CONSTANT (a terrible argument name), the behavior would be different ("paste in" version would use the argument value while this code would use the global constant value).

Further Constraints

Trying to cover any "loopholes" in the problem statement:

  • The exec_str value should represent arbitrary code that can access global or local scope variables.
  • Solution should not require analysis of what global scope variables are accessed within exec_str.
  • There should be no "pollution" between subsequent calls to exec_and_extract (in global namespace or otherwise). i.e. In this example, execution of EXEC_STR should not leave A around to be referenceable in future calls to exec_and_extract.

Solution

  • This is impossible. exec interacts badly with local variable scope mechanics, and it is far too restricted for anything like this to work. In fact, literally any local variable binding operation in the executed string is undefined behavior, including plain assignment, function definitions, class definitions, imports, and more, if you call exec with the default locals. Quoting the docs:

    The default locals act as described for function locals() below: modifications to the default locals dictionary should not be attempted. Pass an explicit locals dictionary if you need to see effects of the code on locals after function exec() returns.

    Additionally, code executed by exec cannot return, break, yield, or perform other control flow on behalf of the caller. It can break loops that are part of the executed code, or return from functions defined in the executed code, but it cannot interact with its caller's control flow.


    If you're willing to sacrifice the requirement to be able to interact with the calling function's locals (as you mentioned in the comments), and you don't care about interacting with the caller's control flow, then you could insert the code's AST into the body of a new function definition and execute that:

    import ast
    import sys
    
    def exec_and_extract(code_string, var):
        original_ast = ast.parse(code_string)
        new_ast = ast.parse('def f(): return ' + var)
        fdef = new_ast.body[0]
        fdef.body = original_ast.body + fdef.body
        code_obj = compile(new_ast, '<string>', 'exec')
    
        gvars = sys._getframe(1).f_globals
        lvars = {}
        exec(code_obj, gvars, lvars)
    
        return lvars['f']()
    

    I've used an AST-based approach instead of string formatting to avoid problems like accidentally inserting extra indentation into triple-quoted strings in the input.

    inspect lets us use the globals of whoever called exec_and_extract, rather than exec_and_extract's own globals, even if the caller is in a different module.

    Functions defined in the executed code see the actual globals rather than a copy.

    The extra wrapper function in the modified AST avoids some scope issues that would occur otherwise; particularly, B wouldn't be able to see A's definition in your example code otherwise.