简单理解Python解释器
Python常见的解释器
- CPython
该解释器是Python官方的解释器。使用C语言开发。在命令行中直接使用Python Shell就是使用的CPython解释器。是使用最广泛的解释器 - IPython
IPython是基于CPython的一个交互式解释器。只是在交互方式上进行了改进。底层运行还是CPython。IPython使用 ‘ In [序号]: ’ 作为提示符。是不是很熟悉。安装Anaconda后,附带的Jupyter NoteBook就是使用此解释器。 - PyPy
PyPy以运行速度为目的。显著的提高了执行速度。与CPython有所不同。所以有可能产生错误。 - Jython
运行在Java平台的Python解释器。将代码编译成Java字节码运行。 - IronPython
运行在微软.Net平台上的Python解释器。编译成.Net的字节码。
解释器干啥了?
解释器将代码编译成字节码执行。Python之所以被称为解释型语言,只是因为他在编译上的工作比重小得多。其他大多数解释型语言也都类似。
一个简单的解释器
我们旨在实现字节码运行的实现。我们假设程序 " 7 + 5 " 的指令集和如下(事实上与实际十分类似,在后面的内容将会看到):
what_to_execute = {
"instructions": [("LOAD_VALUE", 0), # 第一个数
("LOAD_VALUE", 1), # 第二个数
("ADD_TWO_VALUES", None),
("PRINT_ANSWER", None)],
"numbers": [7, 5] }
我们的解释器基于栈来实现。指令 “LOAD_VALUE” 将第一个数压入栈中,指令 “ADD_TWO_VALUES” 从栈顶顺序弹出两个数相加后压入栈中,指令 “PRINT_ANSWER” 将栈顶元素弹出。
这里使用列表来模拟栈,实现每个指令对应的函数,代码如下:
class Interpreter:
def __init__(self):
self.stack = []
def LOAD_VALUE(self, number):
self.stack.append(number)
def PRINT_ANSWER(self):
answer = self.stack.pop()
print(answer)
def ADD_TWO_VALUES(self):
first_num = self.stack.pop()
second_num = self.stack.pop()
total = first_num + second_num
self.stack.append(total)
def run_code(self, what_to_execute):
#指令列表
instructions = what_to_execute["instructions"]
#常数列表
numbers = what_to_execute["numbers"]
#遍历指令列表,一个一个执行
for each_step in instructions:
#得到指令和对应参数
instruction, argument = each_step
if instruction == "LOAD_VALUE":
number = numbers[argument]
self.LOAD_VALUE(number)
elif instruction == "ADD_TWO_VALUES":
self.ADD_TWO_VALUES()
elif instruction == "PRINT_ANSWER":
self.PRINT_ANSWER()
interpreter = Interpreter()
interpreter.run_code(what_to_execute)
实际的字节码
- 使用__code__.co_code查看字节码
进入Python交互式命令行,参考代码查看:
>>> def cond():
... x = 3
... if x < 5:
... return 'yes'
... else:
... return 'no'
...
>>> cond.__code__.co_code
b'd\x01}\x00|\x00d\x02k\x00r\x10d\x03S\x00d\x04S\x00d\x00S\x00'
>>> list(bytearray(cond.__code__.co_code))
[100, 1, 125, 0, 124, 0, 100, 2, 107, 0, 114, 16, 100, 3, 83, 0, 100, 4, 83, 0, 100, 0, 83, 0]
- 使用dis模块查看
标准库中的dis模块,可以实现字节码的反汇编。将字节码以人类可读的方式输出
>>> import dis
>>> dis.dis(cond)
2 0 LOAD_CONST 1 (3)
2 STORE_FAST 0 (x)
3 4 LOAD_FAST 0 (x)
6 LOAD_CONST 2 (5)
8 COMPARE_OP 0 (<)
10 POP_JUMP_IF_FALSE 16
4 12 LOAD_CONST 3 ('yes')
14 RETURN_VALUE
6 >> 16 LOAD_CONST 4 ('no')
18 RETURN_VALUE
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
输出分为5列,分别代表:字节码对应在源码中的行号,字节码在字节码串中的第几个字节,字节码人类可读的命名,字节码参数,字节码参数的内容
参考输出我们来理解一哈:
2 0 LOAD_CONST 1 (3) # 加载常量
2 STORE_FAST 0 (x) # 变量名初始化
3 4 LOAD_FAST 0 (x) # 加载变量
6 LOAD_CONST 2 (5)
8 COMPARE_OP 0 (<) # 弹出栈顶的两个值作小于比较
10 POP_JUMP_IF_FALSE 16 # 结果为假跳转到 16 执行,为真顺序执行
4 12 LOAD_CONST 3 ('yes')
14 RETURN_VALUE
6 >> 16 LOAD_CONST 4 ('no')
18 RETURN_VALUE
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
我们再来看看循环的样子:
>>> def loop():
... x = 1
... while x < 5:
... x = x + 1
... return x
...
>>> dis.dis(loop)
2 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (x)
3 4 SETUP_LOOP 20 (to 26) # 开始循环的标志,在20 到 26 之间循环
>> 6 LOAD_FAST 0 (x)
8 LOAD_CONST 2 (5)
10 COMPARE_OP 0 (<)
12 POP_JUMP_IF_FALSE 24 # 循环退出的标志,跳转 24
4 14 LOAD_FAST 0 (x)
16 LOAD_CONST 1 (1)
18 BINARY_ADD
20 STORE_FAST 0 (x)
22 JUMP_ABSOLUTE 6 # 跳出循环块
>> 24 POP_BLOCK
5 >> 26 LOAD_FAST 0 (x)
28 RETURN_VALUE
>>>
帧
- 通过以上的内容我们已经知道了一个函数内的字节码是如何工作的。那函数之间的呢。这里我们引入一个概念:“帧”。帧包含了一端代码运行时所需的信息与上下文环境。在代码执行时动态的创建与销毁。每一个帧对应一次函数的调用。因为一个函数可以递归调用自己多次,所以一个code object可以拥有多个帧。
- 解释器中常用的两种栈,一种是数据栈,执行字节码操作时使用,上文已经涉及。还有一种叫块栈,用于特定的控制流(循环,异常处理),每一个帧都有自己的数据栈和块栈。
- 在多个函数之间每个函数运行开始时,将函数对应的帧压入栈中(函数的调用栈),结束时将对应的帧弹出,并将return value的值压入下一个帧的数据栈中。就完成了一次函数间的值传递。
总结一哈
python解释器包含常用的三种栈,调用栈,数据栈,块栈。
-
调用栈的运行
解释器首先将源码编译为字节码,创建调用栈,以创建第一个帧开始运行,在这之后不断的新建帧,或弹出帧并拿到返回值,调用栈的长度随之变化,直到第一个创建帧返回值,运行结束。 -
帧的运行
- 每一个帧都有对应的code_object,一条没有参数的指令占据一个字节,有参数的指令占据三个字节,后两个字节为参数。这里的参数因指令的不同而效果不同,比如指令POP_JUMP_IF_FALSE,它的参数指的是跳转目标。BUILD_LIST, 它的参数是列表的个数。LOAD_CONST,它的参数是常量的索引。
- 在一个帧运行时,在一个循环中顺序执行每一条指令(使用指令名和参数完成具体操作)。一个标记量标记当前运行到字节码的哪个位置。直到捕获到异常或运行完所有指令拿到返回值。