python - Python 3 - 如何像直接替换一样执行字符串?
问题描述
问题描述
我很好奇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()
但运行时的执行上下文时A
是B
' 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
A
当从内部调用时,因为是'sfunc()
的执行上下文不包含.A
exec_and_extract
locals()
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
.
解决方案
这是不可能的。exec
与局部变量作用域机制的交互很糟糕,而且对于这样的任何事情来说,它都受到了太多的限制。事实上,在执行字符串中的任何局部变量绑定操作实际上都是未定义的行为,包括普通赋值、函数定义、类定义、导入等,如果您exec
使用默认局部变量调用的话。引用文档:
默认 locals 的行为如下面函数 locals() 所述:不应尝试修改默认 locals 字典。如果您需要在函数 exec() 返回后查看代码对局部变量的影响,请传递显式局部变量字典。
此外,由、、执行的代码exec
不能代表调用者执行其他控制流。它可以循环作为已执行代码的一部分,或从已执行代码中定义的函数循环,但它不能与其调用者的控制流交互。return
break
yield
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
在您的示例代码中看到 ' 的定义。
推荐阅读
- javascript - 添加多次图像下拉屏幕(视频游戏js)
- django - 使用 django 渠道进行电子商务的电子购物车(不使用 ajax)
- java - 为什么我的 jsf 托管 bean 中的列表为空
- reactjs - SwiperSlide onTouchStart/onClick => 触发 slideNext()
- python - 通过 localhost:5000 访问时标记不起作用
- java - 如何获得一个必须循环超过 10000 次才能运行的程序?
- node.js - 无法在 OS 10.15.5 中安装 pm2
- huggingface-transformers - 如何更改拥抱脸转换器的默认缓存目录
- docker - Docker 桌面 - kubernetes 无法启动
- javascript - 如何通过单键启动和停止计数器