1

我已经构建了一个函数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
            )
4

0 回答 0