1

问题描述

我很好奇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()sinceA并且B存在于exec_and_extract'slocals()但运行时的执行上下文时AB' exec_and_extracts 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 definedA当从内部调用时,因为是'sfunc()的执行上下文不包含.Aexec_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_extract's的全部内容复制globals()locals()其中,如果很大,则浪费时间globals()(当然不适用于这个人为的示例)。此外,与“粘贴代码”版本略有不同,因为如果其中一个参数exec_and_extract碰巧是GLOBAL_CONSTANT(一个糟糕的参数名称),则行为会有所不同(“粘贴”版本将使用参数值而这代码将使用全局常量值)。

进一步的限制

试图掩盖问题陈述中的任何“漏洞”:

  • exec_str值应该代表可以访问全局或局部范围变量的任意代码。
  • 解决方案不应要求分析在exec_str.
  • 后续调用之间不应该有“污染” exec_and_extract(在全局命名空间或其他地方)。即在这个例子中,执行不EXEC_STR应该留给A将来调用exec_and_extract.
4

3 回答 3

2

这是不可能的。exec与局部变量作用域机制的交互很糟糕,而且对于这样的任何事情来说,它都受到了太多的限制。事实上,在执行字符串中的任何局部变量绑定操作实际上都是未定义的行为,包括普通赋值、函数定义、类定义、导入等,如果您exec使用默认局部变量调用的话。引用文档

默认 locals 的行为如下面函数 locals() 所述:不应尝试修改默认 locals 字典。如果您需要在函数 exec() 返回后查看代码对局部变量的影响,请传递显式局部变量字典。

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


如果您愿意牺牲能够与调用函数的本地交互的要求(正如您在评论中提到的那样),并且您不关心与调用者的控制流交互,那么您可以将代码的 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在您的示例代码中看到 ' 的定义。

于 2020-04-24T05:29:08.323 回答
0

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

那么如何制作globals()字典的副本并从中检索B呢?

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

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

于 2020-04-24T05:40:57.880 回答
0

@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.

于 2020-04-24T06:19:37.167 回答