dis 是 Python 标准库中用于分析字节码的模块,可以将编译后的指令转换为便于阅读的形式,帮助观察变量访问、运算、函数调用和控制流程的实现方式。
它适合学习程序执行机制、比较不同写法的编译结果,以及辅助定位异常或分析性能问题。反汇编展示的是解释器层面的指令结构,不能直接代替运行测试和性能测量。
一、导入与常见应用场景
1、导入模块
dis 随 Python 提供,无需额外安装。
import dis
本文以 CPython 为分析对象。字节码属于解释器实现细节,指令名称、参数含义和输出格式可能随解释器版本变化,不应视为 Python 语言固定不变的规则。
2、常见应用场景
• 理解执行机制:观察表达式如何加载数据、执行运算并返回结果。
• 分析控制流程:查看条件分支、循环和短路求值对应的指令。
• 比较代码结构:分析不同写法生成的指令及代码对象信息。
• 观察编译优化:检查常量折叠等优化是否发生。
• 辅助调试:结合异常回溯定位发生错误的指令。
3、与相关模块的分工
• dis:分析编译后的字节码与相关信息。
• ast:分析源代码的语法结构。
• inspect:获取函数、类、参数和源码等对象信息。
• timeit:测量小段代码的执行时间。
• cProfile:分析程序运行过程中各函数的调用与耗时。
dis 主要回答 " 代码被编译成了什么结构 ";执行频率和实际耗时通常需要其他工具测量。
二、核心概念
1、源代码、字节码与机器码
源代码(Source Code)是编写的 Python 文本。
在 CPython 的常规执行流程中,源代码经过解析和编译,形成包含字节码的代码对象,再由解释器执行。
字节码(Bytecode)是供解释器处理的指令表示,不等同于处理器直接执行的机器码(Machine Code)。dis 展示的是字节码层面的反汇编,而不是硬件汇编指令。
2、代码对象
代码对象(Code Object)保存编译后的指令及相关信息,例如常量、变量名称和源码位置。
普通 Python 函数通过 __code__ 属性关联代码对象:
代码对象与函数对象不同。函数还关联全局命名空间、默认参数和闭包等执行所需的信息。
3、指令与操作参数
一条字节码指令通常包含操作码(Opcode),并可能带有操作参数。
操作参数不一定是源代码中的数值,也可能表示常量索引、变量索引、运算类型或跳转信息。
阅读时应结合指令名称与参数解释,不宜单独根据参数数字推断作用。
4、求值栈
CPython 的许多指令通过求值栈(Evaluation Stack)传递中间结果。
例如,计算 a + b 时,可以从概念上理解为:
• 取得 a 和 b 的值。
• 执行加法,将结果作为新的中间值。
• 将结果返回给调用方。
实际输出可能将多个动作组合为一条指令,或采用专门化指令,因此不必强求每个源码动作都对应一条独立指令。
三、使用 dis.dis ( ) 查看字节码
1、反汇编函数
dis.dis ( ) 默认将反汇编结果写入标准输出,函数本身返回 None。
这里传入的是函数对象 add,不会调用该函数。若写成 dis.dis ( add ( 2, 3 ) ) ,则会先计算函数结果,再尝试对结果进行反汇编,通常不符合分析目的。
2、阅读输出中的信息
反汇编输出主要包含:
• 源码位置:指令对应的行号或更详细的位置。
• 逻辑标签:用于标记跳转目标等位置。
• 指令名称:说明该指令执行的操作。
• 操作参数:指令使用的数字参数。
• 参数解释:变量名、常量值、运算符或跳转目标等可读信息。
当前输出通常使用逻辑标签表示跳转目标。需要同时查看指令偏移量和更详细的源码位置时,可以使用:
偏移量用于定位字节码中的位置,不是源代码行号;源码位置与字节码指令也不一定一一对应。
3、理解常见指令类别
阅读输出时,可以先关注指令的作用类别:
• LOAD_CONST:加载常量。
• LOAD_FAST、LOAD_FAST_BORROW 等:访问局部变量,具体指令具有不同的内部语义。
• STORE_FAST:将值保存到局部变量。
• LOAD_GLOBAL:查找全局名称或内置名称。
• BINARY_OP:执行由参数指定的二元或原地操作。
• COMPARE_OP:执行比较操作。
• CALL:执行调用相关操作。
• RETURN_VALUE:返回栈顶结果。
这些名称只是阅读入口。实际代码还可能使用组合指令或特化指令,应以当前解释器的输出与对应定义为准。
4、分析源代码字符串
dis 会先编译字符串,再展示反汇编结果,不会执行其中的赋值和乘法。因此,分析时不要求 price 和 quantity 已经有值。
也可以显式创建代码对象:
eval 模式用于表达式,exec 模式用于语句序列。选择不同模式,生成的代码结构也可能不同。
5、控制嵌套代码的展开
depth=0 只显示当前代码对象,不递归展开其中的嵌套代码对象。
省略 depth 时,dis 会继续分析嵌套代码对象。这里控制的是反汇编展示的递归深度,不是程序运行时的递归限制。
四、结构化读取指令与代码信息
1、遍历 Instruction 对象
需要在程序中分析指令时,使用 get_instructions ( ) 比解析打印文本更合适。
get_instructions ( ) 返回迭代器,每次产生一个指令对象(Instruction)。
常用字段包括:
• offset:指令在字节码中的偏移位置。
• opname:可读的指令名称。
• opcode:操作码数值。
• arg:数字形式的操作参数,没有参数时为 None。
• argval:解析后的参数值。
• argrepr:参数的可读描述。
• is_jump_target:是否为其他指令的跳转目标。
2、查看源码位置
位置对象包含起止行号与列偏移。部分位置数据可能为 None,分析程序应考虑信息缺失的情况。
列偏移按源码的 UTF-8 字节偏移表示。源码包含中文等非 ASCII 字符时,不能直接将它当作 Python 字符串的字符索引切片。
3、查看代码对象摘要
结果通常包含函数名称、文件名、参数数量、局部变量数量、栈空间需求、常量和名称等信息。
也可以直接打印:
code_info ( ) 返回字符串,show_code ( ) 直接输出信息。
其中的栈空间信息描述求值栈所需的槽位,不表示函数调用次数、递归深度或内存字节数。
4、使用 Bytecode 对象
Bytecode 将常用分析功能组织在一个对象中:
• info ( ) :返回代码对象摘要字符串。
• dis ( ) :返回反汇编结果字符串。
• 迭代对象:逐项获取 Instruction。
因此,需要将反汇编结果保存到变量时,可使用 Bytecode.dis ( ) ;不宜将 dis.dis ( ) 的返回值当作结果文本。
五、分析条件、循环与编译优化
1、条件分支
阅读时可以依次寻找:
• score 与常量的加载。
• 比较操作。
• 根据比较结果决定执行路径的跳转。
• 各分支的返回操作。
反汇编列出的是可能执行的指令结构。一次函数调用通常只经过其中一条路径,不会按输出顺序执行所有分支。
2、循环结构
循环中通常可以观察到获取迭代器、取得下一项、更新累计值和返回循环位置等操作。
循环体的指令在反汇编中只出现一份,但运行时可能重复执行很多次。因此,静态指令数量不等于实际执行次数。
3、短路求值
or 会根据第一个操作数的真值决定是否需要取得第二个操作数。
该表达式返回某个操作数本身,不会统一转换为 True 或 False。可以结合跳转和返回位置观察这种选择行为。
4、常量折叠
在常见 CPython 编译结果中,纯常量表达式 2 + 3 * 4 会提前计算为 14;第二个函数中的 3 * 4 也可能提前合并为 12,而 value 相关的加法仍需要运行时执行。
这种编译期处理称为常量折叠(Constant Folding)。
它说明源代码中的每个运算符不一定对应一次运行时运算。具体优化范围由编译器决定,不能据此认为所有看似固定的表达式都会被提前计算。
六、运行时特化与内联缓存
1、观察运行后的指令形式
CPython 可以根据运行时情况,对部分操作采用更有针对性的执行形式,这类机制称为特化(Specialization)。
adaptive=True 用于显示可能经过运行时特化的字节码。
重复调用使解释器有机会观察操作数类型等信息,但不保证一定出现某种指令。特化结果受解释器实现、构建配置、调用历史和输入类型影响。
2、查看内联缓存
内联缓存(Inline Cache)用于保存解释器执行某些操作时可复用的信息,通常与前面的指令关联。
show_caches=True 会显示这些缓存条目。它们不是源代码中的独立语句,也不能简单按普通业务操作累计。
当前的 get_instructions ( ) 不再将缓存条目作为独立 Instruction 返回;相关信息可通过指令对象的 cache_info 字段读取。不要将它与 dis.dis ( ) 的缓存显示方式混为一谈。
3、分析结果的边界
默认反汇编与特化视图展示的层次不同。比较两段代码时,应明确是否启用了 adaptive 和缓存显示。
dis 不会提供完整的处理器机器码视图;运行时优化也可能超出普通字节码输出能够直接呈现的范围。
七、异常定位与命令行使用
1、根据异常回溯定位指令
Bytecode.from_traceback ( ) 根据回溯创建分析对象,并标记引发异常的指令位置。
本例可将错误与除法操作联系起来。它提供的是指令位置,具体原因仍需要结合异常类型、输入数据和调用上下文判断。
2、在终端反汇编文件
将需要分析的代码保存为 example.py,然后运行:
该命令读取并编译源文件,输出反汇编结果,不会执行文件中的顶层业务代码。
常用选项包括:
• -C:显示内联缓存。
• -O:显示指令偏移量。
• -P:显示更详细的源码位置。
例如:
这里的 -O 位于 -m dis 之后,是 dis 的显示选项;python -O -m dis example.py 中的 -O 则是解释器的优化选项,两者作用不同。
3、保存反汇编结果
也可以将 dis.Bytecode ( square ) .dis ( ) 返回的字符串写入文件,便于归档或比较。
八、应用举例:比较两种列表构造方式
以下示例比较普通循环和列表推导式的编译结构,并将代码摘要、反汇编结果和指令名称统计保存为报告。
报告保存在 bytecode_report.txt,再次运行会覆盖同名文件。
阅读报告时,可以关注普通循环中的方法访问与调用,以及列表推导式中可能出现的 LIST_APPEND 等操作,理解两种写法的编译结构。
示例只统计各函数当前代码对象中的指令,不递归累计嵌套代码对象;运行时执行次数也不包含在该统计中。
如果需要比较耗时,可以在上述函数定义之后补充:
这些时间还包含函数调用及计时包装的开销,应在相同环境和相同输入下比较,并结合实际业务规模判断。
指令更少不一定意味着执行更快。一个调用指令可能触发大量底层工作,而数据类型、缓存状态和内存分配也会影响耗时。
九、常用函数与类速览
以下列出常用调用形式,省略部分可选参数。
• dis.dis ( x ) :输出反汇编结果,返回 None。
• dis.dis ( x, file=file ) :将反汇编结果写入指定文本流。
• dis.dis ( x, depth=0 ) :只分析当前层级,不递归展开嵌套代码对象。
• dis.dis ( x, show_offsets=True ) :显示指令偏移量。
• dis.dis ( x, show_positions=True ) :显示更详细的源码位置。
• dis.dis ( x, adaptive=True ) :显示可能经过运行时特化的字节码。
• dis.dis ( x, show_caches=True ) :显示内联缓存。
• dis.get_instructions ( x ) :返回 Instruction 对象的迭代器。
• dis.code_info ( x ) :返回代码对象摘要字符串。
• dis.show_code ( x ) :直接输出代码对象摘要。
• dis.Bytecode ( x ) :创建可迭代的字节码分析对象。
• Bytecode.dis ( ) :返回格式化的反汇编字符串。
• Bytecode.info ( ) :返回代码对象摘要字符串。
• dis.Bytecode.from_traceback ( tb ) :根据异常回溯创建分析对象并标记相关指令。
• dis.stack_effect ( opcode, oparg=None, *, jump=None ) :计算指令对求值栈深度的净影响。
十、补充说明
1、字节码与语言语义
Python 语言规定表达式和语句的行为,CPython 字节码描述某种具体实现。
学习时应先理解代码语义,再用反汇编观察实现,不宜将特定指令组合当作编写 Python 程序必须遵守的固定模式。
2、结构分析与性能判断
dis 展示指令结构,不记录每条指令实际执行了多少次,也不直接测量耗时。
发现可疑结构后,应结合 timeit、cProfile 或实际任务测试验证,再决定是否优化。
3、编译分析与代码执行
反汇编普通函数对象或编译源代码字符串,不会调用被分析的函数或执行业务语句。
但为了取得函数而导入模块时,模块顶层代码仍可能执行;特化示例中的重复调用和性能测量也会实际运行目标函数。
4、反汇编与源码恢复
dis 不能完整恢复原始源码。变量名和部分位置信息可能保留,但注释、排版以及某些源码结构已经在编译过程中丢失或改变。
因此,反汇编结果适合分析执行结构,不能替代源码备份或作为完整的反编译结果。
5、栈变化与执行成本
stack_effect ( ) 返回的是某条指令对求值栈深度的净影响,不是执行时间或内存消耗。
对于存在跳转的指令,跳转与不跳转时的栈变化可能不同。计算整个函数的栈行为时,需要结合控制流程,不能简单将所有指令的结果相加。
小结
dis 将 CPython 字节码转换为可读指令,帮助理解变量访问、控制流程、编译优化和运行时特化。分析时应结合源码语义与解释器环境;涉及性能结论,还需通过计时或性能分析工具验证。

"点赞有美意,赞赏是鼓励"


登录后才可以发布评论哦
打开小程序可以发布评论哦