dis.dis()用于反编译函数或代码对象,需传入函数名或compile后的字符串;字节码指令如load_global、binary_add反映cpython底层操作;分析应使用dis.get_instructions()获取instruction对象;不同python版本字节码不兼容,需限定小版本。

dis.dis() 能直接反编译函数或代码对象
想看某段 Python 代码实际执行的字节码,最常用的是 dis.dis()。它接受函数对象、代码对象(__code__)、甚至字符串(需先用 compile() 处理),然后打印人类可读的指令序列。
常见错误是传入调用结果(比如 dis.dis(func())),这会报 TypeError: don't know how to disassemble NoneType object——因为 func() 返回的是运行结果,不是可反编译的对象。
- 正确做法:传函数名本身,如
dis.dis(my_func) - 若要分析表达式字符串,得先编译:
dis.dis(compile("x + 1", "", "eval")) - 类方法需取其
__func__.__code__,实例方法则用method.__func__.__code__
字节码里看到的 LOAD_GLOBAL、BINARY_ADD 是什么
字节码指令名反映底层 CPython 解释器的操作。例如 LOAD_GLOBAL 表示从全局命名空间取变量,BINARY_ADD 是对栈顶两个值做加法(不区分 + 是数字相加还是字符串拼接,具体逻辑由操作数类型在运行时决定)。
注意:同一行源码可能对应多条字节码(比如 a = b + c 拆成 LOAD_GLOBAL、LOAD_GLOBAL、BINARY_ADD、STORE_GLOBAL);而简单常量表达式(如 2 + 3)在编译期就被优化为单条 LOAD_CONST,看不到运算过程。
-
POP_TOP常出现在语句末尾(如函数调用没赋值),表示丢弃返回值 -
RETURN_VALUE总是函数字节码的最后一行,哪怕你没写return,CPython 也会隐式补上 - 带循环或条件的代码会出现
JUMP_FORWARD、POP_JUMP_IF_FALSE等跳转指令,这是控制流的底层体现
dis.get_instructions() 更适合程序化分析字节码
如果不想只打印,而是要检查某条指令是否存在、统计某类操作频次,或做自动化字节码校验,应该用 dis.get_instructions()。它返回一个生成器,每个元素是 Instruction 对象,含 opname、arg、argval、offset 等属性。
容易忽略的是 argval:它把原始操作数(arg)映射为实际值,比如 LOAD_CONST 的 argval 是真正的常量(42 或 "hello"),而 arg 只是索引。直接比对 opname 和 argval 才能写出可靠的分析逻辑。
- 获取所有指令列表:
list(dis.get_instructions(my_func)) - 查找是否用了
import相关操作:any(i.opname in ("IMPORT_NAME", "IMPORT_FROM") for i in dis.get_instructions(func)) - 注意:装饰器会包裹原函数,
get_instructions默认看到的是装饰后代码,要分析原逻辑得取func.__wrapped__.__code__
不同 Python 版本的字节码不兼容,别拿 3.11 的指令去套 3.9
CPython 各版本间字节码格式可能变化。例如 3.11 引入了“自适应指令”(adaptive instructions),部分指令会根据运行时行为动态替换(如 CALL 可能变成 CALL_PY_EXACT_ARGS);3.12 进一步调整了 LOAD_FAST 和 STORE_FAST 的参数编码方式。直接对比不同版本的 dis 输出,很可能发现指令数量、名称、甚至顺序都不同。
这意味着:基于字节码做运行时检查(如防篡改、热补丁)必须绑定具体 Python 小版本;写工具链时,应通过 sys.version_info 明确限定支持范围,而不是假设指令集稳定。
真正难处理的是那些被解释器悄悄优化掉的指令——比如闭包变量访问在某些版本中会从 LOAD_DEREF 变成 LOAD_FAST,表面看逻辑一样,但依赖字节码做静态分析的工具就可能漏判。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











