我之前的一篇博文Python程序的执行过程(解释型语言和编译型语言)给大家提及了Python中的pyc文件的诞生和它的作用,其实是为了提升Python解释器的效率,将py文件编译成了字节码,并保存到了pyc文件中。其中Python实际上是将源代码编译为虚拟机的一组指令(字节码,也叫pycodeobject),Python解释器就是该虚拟机的实现。
Cpython使用基于堆栈的虚拟机,也就是说,它完全围绕堆栈数据结构(你可以将项目“推”到结构的“顶部”,或者将项目“弹出”到“顶部”)去运行。
① 调用堆栈。这是运行中的Python程序的主要结构。对于每个当前活动的函数调用,它都有一个项目 – “帧(Frame)”,堆栈的底部是程序的入口点。每次函数调用都会将新的帧推到调用堆栈上,每次函数调用返回时,它的帧(可以理解为用于传输数据的结构)都会弹出。
② 在每一帧中,都有一个评估堆栈(也称为数据堆栈)。这个堆栈是执行 Python 函数的地方,执行Python代码主要包括将数据推到这个堆栈上,操纵它们,然后将它们弹出。
③ 同样在每一帧中,都有一个块堆栈。Python使用它来跟踪某些类型的控制结构:循环、try /except块,以及 with 块都会导致条目被推送到块堆栈上,每当退出这些结构之一时,块堆栈就会弹出。这有助于Python知道在任何给定时刻哪些块是活动的,例如,continue或break语句可以影响正确的块。
大多数 Python 字节码指令操作的是当前调用栈帧的计算栈,虽然,还有一些指令可以做其它的事情(比如跳转到指定指令,或者操作块栈)。
为了更好的理解,假设我们有一些调用函数的代码,比如下面这个:
my_func(my_var, 6)
上述Python代码在执行时,Python解释器会将其转换为一系列的字节码指令:
LOAD_NAME
指令,用于查找函数对象 my_func
,并将其推送到计算栈的顶部;LOAD_NAME
指令去查找变量 my_var
,并将其推送到计算栈的顶部;LOAD_CONST
指令将一个整数 6
推送到计算栈的顶部;CALL_FUNCTION
指令。CALL_FUNCTION 指令有1个参数,它表示 Python 需要在堆栈顶部弹出1个位置参数;然后函数将在它上面进行调用,并且它也同时被弹出(关键字参数的函数,使用指令 CALL_FUNCTION_KW
类似的操作,并配合使用第三条指令 CALL_FUNCTION_EX
,它适用于函数调用涉及到参数使用 * 或 ** 操作符的情况)
一旦 Python 具备了这些,它将在调用堆栈上分配一个新的帧,填充到函数调用的本地变量,然后运行该帧内的 my_func
的字节码。一旦运行完成,帧将从调用堆栈中弹出,在原始帧中,my_func
的返回值将被推入到计算栈的顶部。
我们知道了这个,也知道字节码文件了,但是如何去使用字节码呢?
不知道也没关系,接下来的时间我们所有的话题都将围绕字节码,在python有一个模块可以通过反编译Python代码来生成字节码,这个模块就是今天要说的 dis
模块。
上图中,通过 dis
模块我们可以很快将 hello()
函数中的Python源代码反汇编成字节码:
1、LOAD_GLOBAL 0:告诉 Python 通过 co_names (它是 print 函数)的索引 0 上的名字去查找它指向的全局对象,然后将它推入到计算栈;
2、LOAD_CONST 1:带入 co_consts 在索引 1 上的字面值,并将它推入(索引 0 上的字面值是 None,它表示在 co_consts 中,因为 Python 函数调用有一个隐式的返回值 None,如果函数没有显式的返回表达式,就返回这个隐式的值 );
3、CALL_FUNCTION 1:告诉 Python 去调用一个函数;它需要从栈中弹出一个位置参数,然后,新的栈顶将被函数调用。
代码对象在函数中可以以属性 __code__
来访问,并且携带了一些重要的属性:
许多字节码指令,尤其是那些推入到栈中的加载值,或者在变量和属性中的存储值,都是以在这些元组中的索引作为它们参数。
其中 co_code
的字节码指令序列我们可以打印出来:
对照dis输出的字节码指令, 以[116,0]序列为例。116表示在Python字节码定义中的索引,在python代码中,可以通过 dis.opname[116]
查看,即为 LOAD_GLOBAL
。而后的1个字节表示指令的参数。而使用 dis
输出的字节码指令中,第二列的字节码索引则是指当前指令在 co_code
序列中所在的位置。
2
表示对应python源代码的行号;co_code
中的索引),字节码指令 LOAD_CONST
在 0
位置,即 co_code
列表中的第一个元素;co_code
是 bytes
类型);co_varnames
和 co_consts
元组中的索引;另外,其中的 >>
表示跳转的目标, 第6行的 16
表明了跳转到索引为 16
的指令,这个指令由 POP_JUMP_IF_FALSE 触发(这行指令后面的 16
即表示跳转到字节码指令索引为 16
的指令去执行)。
Python代码在编译过程中会生成 CodeObject
,CodeObject
是在虚拟机中的抽象表示, 在Python 的 C源码中表示为 PyCodeObject
,而生成的 .pyc
文件则是字节码在磁盘中的存储的文件。
当然对于简单的代码我们可以通过命令行的形式完成 .py
文件中代码的反汇编:
https://www.jb51.net/article/86611.htm
https://www.jb51.net/article/161518.htm