为什么在运行字节码时 Python exec 中缺少局部变量?

问题描述

我构建了一个名为 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_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 参数有关。它们几乎相同,除了实际的字节码 frame.f_code.co_code,我对其进行了一些操作。

我需要帮助理解为什么在这全局变量和局部变量下的代码评估未能引用 main 的局部变量。

注意:我很确定对 main 的字节码所做的更改可以防止进程进入不必要的递归。

编辑:正如评论中所要求的,可以恢复 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
            )

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)