Python 3 - 如何像直接替换一样执行字符串?

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

问题描述

我很好奇是否可以 exec 函数中的字符串,就好像该字符串直接替换 exec 一样(使用适当的缩进)。我知道在 99.9% 的情况下,您不应该使用 exec,但我更感兴趣的是是否可以这样做,而不是是否应该这样做。

我想要的行为等同于:

GLOBAL_CONSTANT = 1

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

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

但我得到的是:

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

尝试失败

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 在调用 func() 时,因为 AB 存在于 exec_and_extractlocals() 中,但是 运行 ABexec_and_extractglobals().


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 当从 func() 内部调用 A 时,因为 A 的执行上下文是 exec_and_extractlocals() 而不是包含 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]

有效但污染全局命名空间,不等同。


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]

可行,但需要将 exec_and_extractglobals() 的全部内容复制到它的 locals() 中,如果 globals() 很大(当然不是适用于这个人为的例子)。此外,与 "paste in code" 版本略有不同,因为如果 exec_and_extract 的参数之一恰好是 GLOBAL_CONSTANT (一个糟糕的参数名称),行为会有所不同("paste in" 版本将使用参数值,而此代码将使用全局常量值)。

进一步的约束

试图覆盖问题陈述中的任何 "loopholes":

这是不可能的。 exec 与局部变量作用域机制的交互很糟糕,而且它对于像这样的任何工作来说都太受限制了。事实上,从字面上看,执行字符串中的任何局部变量绑定操作都是未定义行为,包括普通赋值、函数定义、class 定义、导入等,如果您调用 exec 与默认的当地人。引用 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.

此外,由 exec 执行的代码不能 returnbreakyield 或代表调用者执行其他控制流。它可以 break 作为已执行代码一部分的循环,或 return 来自已执行代码中定义的函数,但它不能与其调用者的控制流交互。


如果您愿意牺牲能够与调用函数的局部变量交互的要求(正如您在评论中提到的),并且您不关心与调用者的控制流交互,那么您可以将代码的 AST 插入到新函数定义的主体中并执行:

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']()

我使用了基于 AST 的方法而不是字符串格式化来避免诸如不小心将额外缩进插入输入中的三引号字符串等问题。

inspect 让我们使用调用 exec_and_extract 的人的全局变量,而不是 exec_and_extract 自己的全局变量,即使调用者在不同的模块中。

在执行的代码中定义的函数看到的是实际的全局变量而不是副本。

修改后的 AST 中的额外包装函数避免了一些否则会发生的范围问题;特别是,B 将无法在您的示例代码中看到 A 的定义。

Works but pollutes global namespace, not equivalent.

那么如何复制 globals() 字典,并从中检索 B

def exec_and_extract(exec_str, var_name):
    env = dict(globals())
    env.update(locals())
    exec(EXEC_STR, env)
    return env[var_name]

这仍然有效,并且不会污染全局命名空间。

@user2357112supportsMonica(响应线程中的评论,因为它包含代码块)

看起来像这样的东西可能会起作用:

def exec_and_extract(exec_str, var_name):
    env = {}
    modified_exec_str = """def wrapper():
{body}
    return {var_name}
    """.format(body=textwrap.indent(exec_str, '    '), var_name=var_name)
    exec(modified_exec_str, globals(), env)
    return env['wrapper']()

这允许访问包括未来更改在内的全局范围以及访问 exec_str 中定义的其他变量。