黑马程序员技术交流社区
标题: 【上海校区】Python 实现 Python 解释器 [打印本页]
作者: 不二晨 时间: 2018-11-5 09:37
标题: 【上海校区】Python 实现 Python 解释器
一、课程介绍1. 课程来源课程内容在原文档基础上做了稍许修改,增加了部分原理介绍,步骤的拆解分析及源代码注释。
2. 内容简介本课程会从实现一个玩具解释器开始学习实现解释器需要的基本知识。之后通过Python的dis库查看真实的Python字节码,进一步理解Python解释器的内部机制。最终参考Byterun(一个现有的Python解释器)实现一个500行以内的Python解释器。
课程来源的原作者 Allison Kaptur 就是Byterun的主要作者之一。Byterun的结构与CPython很像,理解Byterun能够帮助你理解大部分解释器,对于理解CPython更是大有帮助。
3. 课程知识点本课程项目完成过程中,我们将学习:
- Python程序的运行原理
- Python解释器的内部机制
- 如何实现一个Python解释器
- 一些编写Python程序的小技巧
二、实验说明1. Python解释器这里的Python解释器具体是指什么呢?有时候我们会把Python的REPL(命令行下Python的交互环境)当作解释器,有时候Python解释器这一说法可以指代整个Python,它会将源代码编译为字节码并执行。本课程实现的解释器只完成最后一部分执行字节码的工作,也就相当于一个跑Python字节码的Python虚拟机。
你也许会奇怪Python不是解释型语言吗,虚拟机跑字节码那不就像java那种编译型语言了吗。其实这种分类本来就不是很精确的,大部分的解释型语言包括Python都会有编译这个过程。之所以被称作是解释型语言是因为它们在编译上的工作比重相对而言小很多。
2. Python实现的Python解释器本课程的原型-Byterun是一个Python实现的Python解释器,你也许会觉得很奇怪,好比自己生下了自己这种说法一样奇怪。其实也没那么奇怪,你看gcc就是用 C 写的,你也可以使用别的语言来实现python解释器,其实除了实现的功能之外,解释器跟一般的程序并没有什么不同。
使用Python实现Python解释器有优点也有缺点,最大的缺点就是速度,Byterun运行python程序会比CPython慢很多。优点就是我们可以直接使用Python的部分原生实现,比如Python的对象系统。当Byterun需要创建一个类的时候,可以直接使用原Python进行创建。当然最大的优点还是Python代码短小精悍,仅仅500行就能实现一个功能还算完整的解释器,所以说人生苦短,Python是岸呐。
3. Python解释器的结构我们的Python解释器是一个模拟堆栈机器的虚拟机,仅使用多个栈来完成操作。解释器所处理的字节码来自于对源代码进行词法分析、语法分析和编译后所生成的code object中的指令集合。它相当于Python代码的一个中间层表示,好比汇编代码之于C代码。 三、Hello, 解释器让我们从解释器界的hello world开始吧,这个最简单的入门解释器仅实现加法这一个功能。它也只认得三条指令,所以它可以运行的程序也只有这三个指令的排列组合而已。现在听上去挺寒颤的,但在完成本课程的学习后就不一样啦。
入门解释器最基本的三个指令:
- LOAD_VALUE
- ADD_TWO_VALUES
- PRINT_ANSWER
既然我们只关心运行字节码的部分,那就不用去管源代码是如何编译为上述三个指令的某种排列组合的。我们只要照着编译后的内容逐指令运行就行了。从另一个方面看,你要是发明了一种新语言,同时编写了相应的生成字节码的编译器,那就可以在我们的python解释器上跑了呀。
以 7 + 5 作为源码举例, 编译后生成以下指令集合:
what_to_execute = { "instructions": [("LOAD_VALUE", 0), # 第一个数 ("LOAD_VALUE", 1), # 第二个数 ("ADD_TWO_VALUES", None), ("PRINT_ANSWER", None)], "numbers": [7, 5] }
在这里what_to_execute相当于code object, instructions相当于字节码。
我们的解释器是一个堆栈机器,所以是使用栈来完成加法的。首先执行第一个指令 LOAD_VALUE,将第一个数压入栈中,第二个指令同样将第二个数压入栈中。第三个指令 ADD_TWO_VALUES 弹出栈中的两个数,将它们相加并将结果压入栈中,最后一个指令弹出栈中的答案并打印。栈的内容变化如下图所示:
LOAD_VALUE 指令需要找到参数指定的数据进行压栈,那么数据哪里来的呢?可以发现我们的指令集包含两部分:指令自身与一个常量列表。数据来自常量列表。 了解了这些后来写我们的解释器程序。我们使用列表来表示栈,同时编写指令相应的方法模拟指令的运行效果。
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)
运行结果:
尽管我们的解释器现在还很弱,但它执行指令的过程跟真实Python实际上是差不多的,代码里有几个需要注意的地方: - 代码中LOAD_VALUE方法的参数是已读取的常量而不是指令的参数。
- ADD_TWO_VALUES 并不需要任何参数,计算使用的数直接从栈中弹出获得,这也是基于栈的解释器的特性。
我们可以利用现有的指令运行3个数甚至多个数的加法:
what_to_execute = { "instructions": [("LOAD_VALUE", 0), ("LOAD_VALUE", 1), ("ADD_TWO_VALUES", None), ("LOAD_VALUE", 2), ("ADD_TWO_VALUES", None), ("PRINT_ANSWER", None)], "numbers": [7, 5, 8] }
运行结果:
变量下一步我们要在我们的解释器中加入变量这个概念,因此需要新增两个指令:
- STORE_NAME: 存储变量值,将栈顶的内容存入变量中。
- LOAD_NAME: 读取变量值,将变量的内容压栈。
以及新增一个变量名列表。
下面是我们需要运行的指令集合:
#源代码def s(): a = 1 b = 2 print(a + b)#编译后的字节码what_to_execute = { "instructions": [("LOAD_VALUE", 0), ("STORE_NAME", 0), ("LOAD_VALUE", 1), ("STORE_NAME", 1), ("LOAD_NAME", 0), ("LOAD_NAME", 1), ("ADD_TWO_VALUES", None), ("PRINT_ANSWER", None)], "numbers": [1, 2], "names": ["a", "b"] }
因为这里不考虑命名空间和作用域的问题,所以在实现解释器的时候可以直接将变量与常量的映射关系以字典的形式存储在解释 器对象的成员变量中,同时由于多了变量名列表与操作变量名列表的指令,通过指令参数取得方法参数的时候还需根据指令来判断所取的是哪一个列表(常量列表还 是变量名列表),因此需要再实现一个解析指令参数的方法。
带有变量的解释器的代码实现如下:
class Interpreter: def __init__(self): self.stack = [] #存储变量映射关系的字典变量 self.environment = {} def STORE_NAME(self, name): val = self.stack.pop() self.environment[name] = val def LOAD_NAME(self, name): val = self.environment[name] self.stack.append(val) 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 parse_argument(self, instruction, argument, what_to_execute): #解析命令参数 #使用常量列表的方法 numbers = ["LOAD_VALUE"] #使用变量名列表的方法 names = ["LOAD_NAME", "STORE_NAME"] if instruction in numbers: argument = what_to_execute["numbers"][argument] elif instruction in names: argument = what_to_execute["names"][argument] return argument def run_code(self, what_to_execute): instructions = what_to_execute["instructions"] for each_step in instructions: instruction, argument = each_step argument = self.parse_argument(instruction, argument, what_to_execute) if instruction == "LOAD_VALUE": self.LOAD_VALUE(argument) elif instruction == "ADD_TWO_VALUES": self.ADD_TWO_VALUES() elif instruction == "PRINT_ANSWER": self.PRINT_ANSWER() elif instruction == "STORE_NAME": self.STORE_NAME(argument) elif instruction == "LOAD_NAME": self.LOAD_NAME(argument)
运行结果:
相信你已经发现,我们现在才实现五个指令,然而run_code已经看上去有点"肿"了,之后再追加新的指令,它就更"肿"了。不怕,可以利用python的动态方法查找特性。因为指令名与对应的实现方法名是相同的,所以可以利用getattr方法,getattr会根据输入的方法名返回对应的方法,这样就可以摆脱臃肿的分支结构,同时再追加新指令也不用修改原来的run_code代码了。 下面是run_code的进化版execute:
def execute(self, what_to_execute): instructions = what_to_execute["instructions"] for each_step in instructions: instruction, argument = each_step argument = self.parse_argument(instruction, argument, what_to_execute) bytecode_method = getattr(self, instruction) if argument is None: bytecode_method() else: bytecode_method(argument)
【转载】https://juejin.im/entry/57b2c18b5bbb500062e53c3d
作者: 不二晨 时间: 2018-11-7 09:06
ヾ(◍°∇°◍)ノ゙
作者: 梦缠绕的时候 时间: 2018-11-8 16:53
作者: 魔都黑马少年梦 时间: 2018-11-8 17:05
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) |
黑马程序员IT技术论坛 X3.2 |