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_const
或co_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
模块可深入了解并查看字节码。 -
虚拟机值得仔细研究。