Python 3如何执行字符串,就好像它被直接替换一样?

2024-03-28 08:34:25 发布

您现在位置:Python中文网/ 问答频道 /正文

问题描述

我很好奇,是否可以在函数中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 definedfunc(),因为AB存在于exec_and_extractlocals()中,但运行AB时的执行上下文是exec_and_extract{}


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 definedfunc()内部调用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()很大,这是浪费时间的(当然在这个人为的示例中不适用)。此外,它与“粘贴在代码中”版本有细微的不同,因为如果exec_and_extract的一个参数恰好是GLOBAL_CONSTANT(一个糟糕的参数名称),则行为会有所不同(“粘贴在”版本将使用参数值,而此代码将使用全局常量值)

进一步的限制

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

  • exec_str值应表示可以访问全局或局部范围变量的任意代码
  • 解决方案不应要求分析在exec_str中访问哪些全局范围变量
  • exec_and_extract的后续调用之间不应该有“污染”(在全局名称空间中或其他地方)。i、 在这个例子中,EXEC_STR的执行不应该让A在未来对exec_and_extract的调用中是可引用的

Tags: andnamereturnvardefextractglobalexec
3条回答

Works but pollutes global namespace, not equivalent.

那么,制作一份globals()dict的副本并从中检索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中定义的其他变量

这是不可能的exec与局部变量作用域机制的交互不好,而且这种机制太受限制,无法工作。事实上,如果您使用默认的局部变量调用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的定义

相关问题 更多 >