Python 一窥字节码的究竟

Python 一窥字节码的究竟,CPython解释器执行程序时,首先将其翻译成一系列的字节码指令。字节码是Python虚拟机的中间语言,可以提高程序的执行效率。

CPython解释器不直接执行人类可读的源码,而是执行由编译器解析和语法语义分析产生的紧凑的数、常量和引用。
这样,再次执行相同程序时能节省时间和内存。因为编译步骤产生的字节码会以.pyc和.pyo文件的形式缓存在磁盘上,所以执行字节码比再次解析并执行相同的Python文件速度更快。
对程序员来说所有这些步骤都是完全透明的,无须在意这些中间转换步骤,也无须在意Python虚拟机如何处理字节码。实际上,字节码格式是实现细节,在各Python版本之间并不一定保持稳定或兼容。
窥探CPython解释器内部并了解其工作原理能够提升自己、获得启发。了解这些知识不仅能带来乐趣,更重要的是有助于编写更高效的代码。
以下面简单的greet()函数作为实验样本,从中学习Python字节码:

def greet(name):
    return 'Hello, ' + name + '!'

>>> greet('Guido')
'Hello, Guido!'

前面说过,CPython在运行这段代码之前首先将其转换为中间语言。如果这种说法是真的,那么应该能够看到这个编译步骤的结果。我们也确实可以看到。
Python 3中,每个函数都有__code__属性,这个属性可以获取greet函数用到的虚拟机指令、常量和变量:

>>> greet.__code__.co_code
b'd\x01|\x00\x17\x00d\x02\x17\x00S\x00'
>>> greet.__code__.co_consts
(None, 'Hello, ', '!')
>>> greet.__code__.co_varnames
('name',)

可以看到,co_consts中含有greet函数中用来组装问候语的字符串。同时常量和代码分开存储以节省存储空间。常量是恒定的,永远不会改变,可以在多个地方互换使用。
因此,Python没有在co_code指令流中重复存储实际的常量值,而是将Python中的常量单独存储在查找表中。之后指令流使用查找表中的索引来引用常量,存储在co_varnames字段中的变量也是如此。
我希望这个总体思想已经逐步明朗了,但看着co_code中复杂的指令流,似乎有点不现实。这种中间语言显然更适合CPython虚拟机使用,基于文本的源码才是供人类阅读的。
CPython的开发人员也意识到了这一点,所以提供了另一个称为反汇编器的工具,以便更容易地查看字节码。
CPython的字节码反汇编程序位于标准库的dis模块中。将其导入并在greet函数中调用dis.dis()就能以稍微易于阅读的形式显示对应的字节码:

>>> import dis
>>> dis.dis(greet)
   2           0 LOAD_CONST             1('Hello, ')
               2 LOAD_FAST              0(name)
               4 BINARY_ADD
               6 LOAD_CONST             2('!')
               8 BINARY_ADD
              10 RETURN_VALUE

反汇编的主要工作是划分指令流,并为其中的每个操作码(opcode)赋予一个人类可读的名称,如LOAD_CONST
从中还可以看到常量和变量引用与字节码隔开了一段距离,其中的值也完整地打印了出来,省得我们根据索引在co_constco_varnames表中手动查看,很不错吧!
通过这些人类可读的操作码,现在可以开始理解CPython如何表示和执行原greet()函数中的'Hello',+ name +'!'表达式了。
解释器首先在索引1处('Hello, ')查找常量并将其放在上,然后将name变量的内容放在上。
这个数据结构用作虚拟机的内部存储空间。虚拟机有不同的种类,其中有一种称为栈式虚拟机,CPython虚拟机就是这种实现。既然以栈命名这种虚拟机,那么就不难看出这个数据结构在其中的重要性。
顺便说一句,这里只是介绍了一些皮毛。如果你对这个主题有兴趣,可以看看本节最后推荐的一本书。钻研虚拟机理论不仅能带来很多收获,也能带来很多乐趣。
作为抽象数据结构,其有趣之处在于只支持两种操作:入栈出栈入栈将一个值添加到栈顶,出栈删除并返回栈顶的值。与数组不同,栈无法访问栈顶下面的元素。
栈令人着迷,这么简单的数据结构却有着非常多的用途。不过这次我不会跑题了……
假设栈初始为空,在执行前两个操作码之后,虚拟机栈的内容(0是最上面的元素)如下所示:

0: 'Guido' (contents of "name")
1: 'Hello, '

BINARY_ADD指令从栈中弹出两个字符串值,并将它们连接起来,然后再次将结果压入栈:

0: 'Hello, Guido'

然后由另一个LOAD_CONST将感叹号字符串压入栈:

0: '!'
1: 'Hello, Guido'

下一个BINARY_ADD操作码再次将这两个字符串连接起来以生成最终的问候语字符串:

0: 'Hello, Guido!'

最后的字节码指令是RETURN_VALUE,它告诉虚拟机当前位于栈顶的是该函数的返回值,可以传递给调用者。
瞧,我们刚刚跟踪了greet()函数在CPython虚拟机内部的执行过程,很棒吧?
关于虚拟机还有许多内容,但这不是本书的主题。如果你对这个迷人的主题感兴趣,我强烈建议阅读更多相关内容。
定义自己的字节码语言并尝试为之构建虚拟机会很有乐趣。关于虚拟机主题的书我推荐Wilhelm和Seidl所著的《编译器设计:虚拟机》(Compiler Design: Virtual Machines)。

关键要点

  • CPython首先将程序转换为中间字节码,然后在基于栈的虚拟机上运行字节码来执行程序。

  • 使用内置的dis模块可深入了解并查看字节码。

  • 虚拟机值得仔细研究。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程