如何通过Python将代码字符串转为代码对象并替换已有函数实现?
我需要修改一个函数的行为,但无法访问或修改该函数所在的文件。不过我可以import它,想要对其实现做一些修改。但由于无法访问源代码,我无法直接修改它。我希望真正改变该函数的实现,使得所有import它的其他文件也能使用新的函数。
我目前尝试操作函数的__code__.co_code属性的字节码(我知道这是非常不好的软件实践,但我真的别无选择)。__code__.co_code返回一系列字节字符,这非常难以理解和修改(我是说,怎么用字节写代码?)。我想向该函数注入一个新的代码对象。有没有办法先将包含新实现的Python字符串转换为字节序列,然后安全地注入到旧函数中?
作为最小可复现示例,假设我有以下函数:
def func1(): return 1
我想将其修改为:
def func1(): return 0
我已经成功访问了函数__code__.co_code属性的字节码序列,代码如下:
def func1(): return 1 code_obj = func1.__code__.co_code print(code_obj) # 输出 b'd\x01S\x00' new_code = b'd\x01S\x00' # 复制了原始代码字节序列。但我该怎么用字节写`return 0`? func1.__code__.co_code = new_code print(func1) code_obj2 = func1.__code__.co_code
但出现了如下错误:
Traceback (most recent call last): File "C:\Program Files\JetBrains\PyCharm 2024.1\plugins\python\helpers\pydev\pydevconsole.py", line 364, in runcode coro = func() File "<input>", line 7, in <module> AttributeError: readonly attribute
错误提示我无法修改函数的__code__.co_code属性。此外,我真的不知道如何用字节序列编写新的函数实现(return 0)。我知道可能可以使用AST,但我也不知道如何使用。我真的被困住了。
补充说明:我不能用新实现遮蔽旧函数,因为我希望其他导入该库的文件也能生效。同时,我也不想通过import lib; lib.old_function = new_function的方式修改模块中的函数。
首先,你碰到的AttributeError是因为__code__.co_code是只读属性,没法直接修改,但函数本身的__code__属性是可以替换的——这才是正确的修改路径,不用跟字节码死磕。
最简单的实现方式:用临时函数替换代码对象
你可以先写一个和目标行为一致的临时函数,然后把它的代码对象直接赋值给原函数的__code__:
def func1(): return 1 # 先定义你想要的目标函数 def _temp_func(): return 0 # 直接替换func1的代码对象 func1.__code__ = _temp_func.__code__ # 测试一下,现在会返回0 print(func1())
这样修改后,所有后续调用func1的地方(包括其他模块导入的)都会使用新的实现——前提是你在其他模块导入func1之前完成这个替换操作。
如果一定要手动处理字节码(不推荐)
要是你非得跟字节码打交道,先搞清楚原字节码的含义:b'd\x01S\x00'里,d\x01是LOAD_CONST 1(加载常量池里的第1个元素,也就是1),S\x00是RETURN_VALUE(返回结果)。要改成返回0,只需要把常量索引改成0,也就是字节码b'd\x00S\x00'。
但因为co_code只读,你需要用types.CodeType创建一个新的代码对象,再替换原函数的__code__:
import types def func1(): return 1 original_code = func1.__code__ # 复制原代码对象的所有参数,只替换co_code new_code_obj = types.CodeType( original_code.co_argcount, original_code.co_posonlyargcount, original_code.co_kwonlyargcount, original_code.co_nlocals, original_code.co_stacksize, original_code.co_flags, b'd\x00S\x00', # 改成返回0的字节码 original_code.co_consts, original_code.co_names, original_code.co_varnames, original_code.co_filename, original_code.co_name, original_code.co_firstlineno, original_code.co_lnotab, original_code.co_freevars, original_code.co_cellvars ) func1.__code__ = new_code_obj print(func1()) # 输出0
这种方式需要严格对应CodeType的所有参数,稍有不慎就会出错,除非你对Python字节码非常熟悉,否则不建议用。
关于跨模块生效的说明
只要你在程序启动的最开始(比如主入口文件的最顶部)完成这个替换,之后任何模块导入func1都会拿到修改后的版本。比如如果func1在第三方库mylib里,你可以这么做:
# 主入口文件第一行 import mylib def _temp_func(): return 0 mylib.func1.__code__ = _temp_func.__code__ # 之后再导入其他模块 import other_module # other_module里调用func1()会返回0
内容的提问来源于stack exchange,提问作者Luke L

