我已经构建了一个函数foo
,用于在字节码级别更改函数的代码并在返回常规函数执行流程之前执行它。
import sys
from types import CodeType
def foo():
frame = sys._getframe(1) # get main's frame
main_code: CodeType = do_something(frame.f_code) # modify function code
# copy globals & locals
main_globals: dict = frame.f_globals.copy()
main_locals: dict = frame.f_locals.copy()
# execute altered bytecode before returning to regular code
exec(main_code, main_globals, main_locals)
return
def main():
bar: list = []
# run altered code
foo()
# return to regular code
bar.append(0)
return bar
if __name__ == '__main__':
main()
但是,在 期间对局部变量的评估存在问题exec
:
Traceback (most recent call last):
File "C:\Users\Pedro\main.py", line 31, in <module>
main()
File "C:\Users\Pedro\main.py", line 23, in main
foo()
File "C:\Users\Pedro\main.py", line 15, in foo
exec(main_code, main_globals, main_locals)
File "C:\Users\Pedro\main.py", line 26, in main
bar.append(0)
UnboundLocalError: local variable 'bar' referenced before assignment
main_locals
如果我在调用它之前打印exec
它会显示与调用之前完全相同的内容foo
。我想知道它是否与frame.f_code.co_*
传递给CodeType
构造函数的任何参数有关。它们几乎相同,除了实际的 bytecode frame.f_code.co_code
,我对其进行了一些操作。
我需要帮助来理解为什么在这些全局变量和局部变量下对代码的评估无法引用main
's 的局部变量。
注意:我很确定对main
's 字节码所做的更改可以防止进程进入不必要的递归。
编辑:do_something
如评论中所问,可以恢复的基本行为以main
在调用foo
. 一些额外的步骤将涉及对局部变量应用更改,即bar
.
import copy
import dis
## dump opcodes into global scope
globals().update(dis.opmap)
NULL = 0
def do_something(f_code) -> CodeType:
bytecode = f_code.co_code
f_consts = copy.deepcopy(f_code.co_consts)
for i in range(0, len(bytecode), 2):
cmd, arg = bytecode[i], bytecode[i+1]
# watch for the first occurence of calling 'foo'
if cmd == LOAD_GLOBAL and f_code.co_names[arg] == 'foo':
break # use 'i' variable later
else:
raise NameError('foo is not defined.')
f_bytelist = list(bytecode)
f_bytelist[i:i+4] = [
NOP, NULL, ## LOAD
LOAD_CONST, len(f_consts) ## CALL
# Constant 'None' will be added to 'f_consts'
]
f_bytelist[-2:] = [NOP, NULL] # 'main' function RETURN
# This piece of code removes all code before
# calling 'foo' (except for JUMP_ABSOLUTE) so
# it can be usend inside while loops.
null_code = [True] * i
j = i + 2
while j < len(f_bytelist):
if j >= i:
cmd, arg = f_bytelist[j], f_bytelist[j+1]
if cmd == JUMP_ABSOLUTE and arg < i and null_code[arg]:
j = arg
else:
j += 2
else:
null_code[j] = False
j += 2
else:
for j in range(0, i, 2):
if null_code[j]:
f_bytelist[j:j+2] = [NOP, NULL] # skip instruction
else:
continue
f_bytecode = bytes(f_bytelist)
f_consts = f_consts + (None,) ## Add constant to return
return CodeType(
f_code.co_argcount,
f_code.co_kwonlyargcount,
f_code.co_posonlyargcount, # Remove this if Python < 3.8
f_code.co_nlocals,
f_code.co_stacksize,
f_code.co_flags,
f_bytecode,
f_consts,
f_code.co_names,
f_code.co_varnames,
f_code.co_filename,
f_code.co_name,
f_code.co_firstlineno,
f_code.co_lnotab,
f_code.co_freevars,
f_code.co_cellvars
)