dis.dis() 能直接反编译函数、类、方法、代码对象或编译后的源码字符串,但不能处理模块路径、未编译.py文件或非可调用/非代码对象(如整数、列表),传入模块名(如math)会报错。

dis.dis() 能直接反编译哪些对象?
dis.dis() 接收函数、类、方法、代码对象(code object)或源码字符串,但不能直接传模块文件路径或未编译的.py文件内容。传入非可调用/非代码对象(比如整数、列表)会报 TypeError: don't know how to disassemble ...。
常见误操作是试图对模块名(如 dis.dis(math))调用,这会失败——必须指向具体可执行单元:
- 函数:直接传函数名,如
dis.dis(my_func) - lambda:
dis.dis(lambda x: x + 1)合法 - 源码字符串:需加
compile()预处理,例如dis.dis(compile('a = 1', '', 'exec')) - 类:传入类本身(不是实例),它会反编译其
__init__和其他方法的字节码(Python 3.7+ 默认只显示类体顶层指令,不递归展开方法;如需看某方法,得单独传MyClass.method)
为什么有时看不到预期的LOAD_NAME或CALL_FUNCTION?
Python 3.7+ 引入了自适应字节码(adaptive bytecode),部分操作(如属性访问、函数调用)在首次运行后可能被优化为更专用的指令(如 LOAD_ATTR → LOAD_METHOD,CALL_FUNCTION → CALL_METHOD)。但 dis.dis() 显示的是**编译后的初始字节码**,不是运行时动态替换后的版本。
这意味着你看到的可能是“未优化形态”,而实际执行走的是更快路径。想确认真实执行指令,得结合 sys.settrace() 或使用 dis.Bytecode 的 .get_instructions() 并观察 co_code 原始字节流——但通常没必要,除非你在调试 CPython 内部行为。
另一个干扰源是常量折叠:像 2 + 3 这种表达式,在编译期就被算成 5,字节码里只剩 LOAD_CONST,不会出现 BINARY_ADD。
如何对比不同写法的字节码差异?
用 dis.dis() 对比时,关键不是数指令条数,而是看控制流和对象操作模式。例如:
def f1(): return [x for x in range(3)] def f2(): return list(range(3))
前者生成 LIST_COMP 指令并调用内部生成器,后者是 CALL_FUNCTION + BUILD_LIST。这种差异影响内存分配和迭代开销。
实操建议:
- 确保两个函数处于相同作用域(避免闭包变量引入额外
LOAD_DEREF) - 用
dis.Bytecode(func).codeobj.co_consts查看常量池,确认是否因字符串/数字重复导致常量复用 - 对带条件分支的函数,注意
POP_JUMP_IF_TRUE/JUMP_ABSOLUTE的目标偏移是否合理——偏移错位常意味着逻辑错误或嵌套过深
dis 模块无法显示的隐藏信息有哪些?
dis 只展示 CPython 解释器能解析的字节码指令,不反映:
- 内联缓存状态(如
LOAD_ATTR的快速路径是否已填充) - 垃圾回收相关标记(如对象是否被标记为不可达)
- 解释器栈帧的实际布局(
dis不告诉你当前栈深度或局部变量槽位占用) - 某些 C 扩展模块的调用跳转(如 NumPy 数组操作最终落入 C 函数,字节码里只体现一次
CALL_FUNCTION)
如果你看到一段代码字节码很短,但执行慢,别急着优化字节码——瓶颈很可能在底层 C 实现或 I/O 等待上。这时候该用 cProfile 或 perf,而不是盯着 dis 输出。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











