第2篇:CPython实现原理:字节码的执行
经过前一篇的了解,我们已经知道Python解释器实际执行的是编译后的字节码,本篇着重了解一下Python解释器执行字节码中内置操作码的流程,那么我们看一下下面示例
def fib(n):
if n<2:
return n
current,next=0,1
while n:
current,next=next,current+next
n-=1
return current
因为fib是一个Python函数,而在Python中函数都是一个对象,当然也继承Python对象的内置属性 __ code__
from fib import fib
fib. __code__
__ code__ 属性返回fib函数的代码对象,而代码对象的co_code属性可以返回其字节码序列,我们知道代码对象包含了Python执行该含书所需要的所有信息:指令(操作码),常量和局部变量
我们先来看看co_consts属性,返回一个元组,它包含了含书主题中引用的所有文字或常量值
>>fib.__code__.co_consts
(None, 2, 0, 1, (0, 1))
常量2,0,1和一个包含0和1的元组,还有None,这个None的含义我们稍后再说,因为fib函数主体放在Python任何地方都没有包含文字,这样做的原因,如果Python正在执行我们的函数,并且没有到达任何显式return的语句情况下完成执行,它将返回None,所以Python需要确保载入None,并放入co_consts所指向的元组,因此任何Python函数都可以引用None,Python在编译时,它无法知道是否会到达任何显式的return语句,这种情况下Python解释器可以引用None并将其作为函数的返回值。
我们还有另一个属性叫co_varnames,该属性返回函数的所有局部变量的名称
>>fib.__code__.co_varnames
('n', 'current', 'next')
在其输出的元组中有n,current,next
而在一个co_names的属性中,即可以返回含书中引用的任何非本地名称,因为我们fib函数中没有调用其他def函数或Python的C级别函数,因此返回一个空的元组
>>fib.__code__.co_names
()
最后一个要介绍的是co_code,它是返回fib函数已编译后的字节序列
>>fib.__code__.co_code
下面的字节码序列就是Python3命令行中完成的,因此某些字节会打印成ASCII字符,因为Python shell环境下是以ASCII编码来显示字节对象的,因此需要警惕的是,我们不能将其视为字符串
ss8.png
之前说过字节码实质上就是指令(操作码)的序列,因此,我们需要了解其内容时,第一个字节对应的是什么操作码?Python函数有一个ASCII转换整数的形式
>>ord('|')
124
那么,124整个操作码,又如何表示我们人类能够理解的语义呢?可以使用dis模块中的opname数组
import dis
>>dis.opnames[124]
'LOAD_FAST'
124所标识的操作码就是LOAD_FAST,如此类推,opnames的每个索引值都是十进制的操作码整数其返回的字符串就是对应操作码所表示的含义。
解读字节码
更方便快捷的方式,可以使用dis模块中的dis函数
import dis
dis.dis(fib.__code__.co_code)
输出,下面打印的就是人类可理解的fib函数的字节码执行流程,解读这个反编译序列,需要一些方法,
-
首先反编译序列的第一列2、3、4、5、6、7、8其实就是对应Python源代码中的行号,如下图所示。
ss8.png - 然后每条指令旁边有个数字,这是Python3.6的新特性,如果你在该字节序列的对象代码获取了该字节对象的co_code,即这些属性是该字节码的偏移量,并且也是外部for迭代语句可访问到的索引依据。
从Python3.6开始,在字节码中,请谨记如下特性
- 绝大部分指令占用两个字节,一个指令中的前一个字节表示操作码(CPython的C代码实现定义的整数类型),后一个字节表示该指令需要的参数,并不是所有指令都需要指令参数,没有参数的指令的第二个字节以0填充作为填充字节。(这个是理解字节码的非常关键的概念,请看前一篇随笔)
- 对于一些指令,如果它们的参数太大而无法容纳一个字节,则实际上可以拆分为多个字节,但是如果你使用的是Python3.5或更早的版本,则第二个字节位置的尺寸总是2的倍数,并且使用相同函数的情况下,你可能会得到一些奇数的偏移量,因为并非所有内容都在Python3.5中
值得一提的是,我们看到上图有尖括号,例如第4偏移量为12,第5行偏移量22,第7行偏移量为50,第8行偏移量52,这些指令都是在Python函数执行流程中跳转目标(Jump Target),这是Python告诉你,这些指令可能被此处正在执行的其他指令所跳转。
在上面示例的fib函数中,我们有一个while循环,每次我们必须转到循环的开头时,在判断当前循环中的代码是否执行都需要执行条件判断。对于字节码层面来说,带>>的指令行是其他指令的潜在跳转目标
Python VM中的栈
之前说过Python VM是基于栈的虚拟机,我们知道栈是跟踪函数调用的基本数据结构
更新中...