【问题标题】:Python 3 - How to exec a string as if it were substituted directly?Python 3 - 如何像直接替换一样执行字符串?
【发布时间】:2020-04-24 05:22:31
【问题描述】:

问题描述

我很好奇是否可以在函数中使用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() 中,但是运行AB 时的执行上下文是exec_and_extract 的@987654 @。


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(在全局命名空间或其他地方)之间不应存在“污染”。即在此示例中,EXEC_STR 的执行不应留下 A 以供将来调用 exec_and_extract 时参考。

【问题讨论】:

  • "如果 globals() 很大,那是浪费时间" 不是真的,不是。我的意思是,你意识到,实际上并没有复制任何对象。
  • @juanpa.arrivillaga 是的,当然,只为其他所有内容复制原语和对象“引用”的值(不确定正确的术语是什么)。这可能没有我想象的那么昂贵,但也存在user2357112 supports Monica 在下面指出的问题,它拍摄了globals() 的快照并且看不到这些变量的任何未来更新。
  • 不,它不会复制“primitives”的值,python 没有“primitives”,一切都是对象,这里一切都使用引用语义。但是,是的,全局快照问题确实存在。我只是说,这绝对不是一项昂贵的手术。最多,一毫秒?这可能是一个异常大的globals(),否则你在任何现代机器上都需要数百纳秒。
  • 感谢您澄清 python 复制语义。

标签: python python-3.x python-exec


【解决方案1】:

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

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

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


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

【讨论】:

  • 感谢您的快速回复!如果我放宽要求以使其不等价但仍可运行怎么办。特别是我不需要execed 字符串访问或修改exec_and_extract 函数的locals(),但我仍然希望它能够访问execed 字符串中定义的全局范围变量和其他变量(如B 访问 A)。因此不再需要使用默认的locals()
  • @nj3:还有冲突。如果您希望执行的代码看到原始全局变量范围而不是副本,则必须将实际的 globals() 传递给 exec 而不是副本。如果您不希望执行代码中定义的任何内容污染全局变量,则必须传递一个(非默认)locals dict。反过来,这意味着在执行代码中定义的函数只能看到全局变量,而不是局部变量(这是由于 exec 和闭包机制的相互作用)。特别是在执行代码中定义的函数不能互相调用。
  • 我明白了,所以得出的结论是调整后的问题定义的确切语义(请参阅exec_str 中定义的全局变量和其他变量)是不可能的。正如其他人所指出的,如果没有看到未来对全局范围变量的更改可以,则可以将(非默认)本地字典复制为globals()(应该很快)并将其传递给exec。再次感谢您的帮助!
  • @user2357112supportsMonica 该死的,我在想可能像传递collections.ChainMap({}, globals()) 这样它不会污染全局命名空间但仍然可以读取它会是一个聪明的黑客,但是唉,@987654347 @ 参数必须是dict
  • @juanpa.arrivillaga 哈哈,出于同样的原因,我也在考虑某种链图解决方案,但不幸的是不可能。
【解决方案2】:

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

那么如何复制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]

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

【讨论】:

  • 这更接近(修改后的)目标,但仍然不等价。一个问题是在执行代码中定义的函数看不到对原始全局变量的更新。
  • 谢谢!查看上次失败的尝试。除了将全局变量复制到本地变量而不是新字典之外,基本上等效(如果我不想像其他评论者所说的那样与locals() 混淆,这个解决方案肯定会更好)。也就是说,它具有相同的缺点,即需要所有全局变量的副本。编辑:也同意^,关于全局变量的更改非常好。谢谢!
  • 我编辑了我的答案以解决“如果GLOBAL_CONSTANT 是一个参数名称”问题。至于执行的代码能够看到全局命名空间的变化(由于其他线程或其他原因)......是的,我怀疑这可以完全解决,不幸的是。
  • 感谢您的反馈。是的,这个问题似乎无法完全解决。此外,在执行返回的函数之前调用exec_and_extract 之后,也可以只更改全局更改,不需要并发。
【解决方案3】:

@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 中定义的其他变量。

【讨论】:

  • 我会选择ast 模块而不是字符串格式,以避免意外将虚假缩进插入输入中的三引号字符串等问题。 (实际上,我现在正在编写一个基于ast 的方法。)
猜你喜欢
  • 2018-12-11
  • 2011-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-08
  • 2017-03-09
  • 1970-01-01
相关资源
最近更新 更多