[译]Python编写虚拟解释器
使用Python编写虚拟机解释器
一、实验说明
1. 环境登录
无需密码自动登录,系统用户名shiyanlou,密码shiyanlou
2. 环境介绍
本实验环境采用带桌面的Ubuntu Linux环境,实验中会用到程序:
1. LX终端(LXTerminal):Linux命令行终端,打开后会进入Bash环境,可以使用Linux命令
2. GVim:非常好用的编辑器,最简单的用法可以参考课程Vim编辑器
3. 环境使用
使用R语言交互式环境输入实验所需的代码及文件,使用LX终端(LXTerminal)运行所需命令进行操作。
完成实验后可以点击桌面上方的“实验截图”保存并分享实验结果到微博,向好友展示自己的学习进度。实验楼提供后台系统截图,可以真实有效证明您已经完成了实验。
实验记录页面可以在“我的主页”中查看,其中含有每次实验的截图及笔记,以及每次实验的有效学习时间(指的是在实验桌面内操作的时间,如果没有操作,系统会记录为发呆时间)。这些都是您学习的真实性证明。
二、课程介绍
众所周知,python语言作为一门超级人性化的语言越来越被受到重视。虚拟服务同样受到人们的重视,就比如实验楼的虚拟实验环境,那么本次项目的目的就是让大家学会使用python制作一个虚拟解释器,这里的虚拟解释器指的是一定意义上的堆栈机。
感谢Christian Stigen Larsen的开源项目Crianza,那么我们就跟着他一起学习如何创建一个解释器吧!
三、课程内容
>解释器是能够执行用其他计算机语言编写的程序的系统软件,它是一种翻译程序。它的执行方式是一边翻译一边执行,因此其执行效率一般偏低,但是解释器的实现较为简单,而且编写源程序的高级语言可以使用更加灵活和富于表现力的语法。
1、构建堆栈机
堆栈机本身并不拥有寄存器,它的执行原理非常简单:将需要处理的值放入栈中,然后执行它们。尽管堆栈机的原理就是这么简单,但是不能不说它确实很强大,不然Python、Java等高级语言也不会将它作为它们的虚拟机。
无论如何,先来深入了解一下堆栈的原理。首先,我们需要一个指令指针栈,它能够储存返回地址。这个返回地址是当我们执行一个子例程(比如函数)的时候,需要用它跳回到开始调用该函数的地方。
那么有了这个神奇的堆栈,很多复杂难以理解的程序就变得非常简单。比如说,有这么一个数学表达式:(2+3)*4。在堆栈机中,这个数学表达式等价于2 3 + 4 * ——将'2'和'3'依次推入栈中,接下来要推入的指令是'+',将前面两个数字弹出,令他们执行加法运算后再将它们的和入栈。然后依次将'2'与'3'的和与4相乘的结果推入栈中。运算结束,so easy!
那么,让我们开始建立一个栈,由于Python这个语言拥有类似于C语言中数据结构的一个类collections.deque,因此可以在这个类的基础上定义出属于我们的栈。
from collections import deque class Stack(deque):
"""定义一个栈类"""
# 添加元素
push = deque.append # 返回最后一个元素
def top(self):
return self[-1]
那么这里面既然定义了'push'、'top'方法,为什么没有定义'pop'?因为'deque'这个类本身就拥有方法'pop',除了'pop'还有'popleft'呢,有兴趣的同学可以研究一下这个类与'list'对象的区别和联系。
接下来,让我们建立一个虚拟机类——'Machine'。综上所述,我们需要两个栈和一段存储代码的内存空间。得益于Python的动态类型,因此我们可以往列表里面存储任何东西,但是我们不能区分列表里面的内置函数和字符串,正确的做法是将Python内置函数单独存放于一个列表,关于这个问题大家可以思考一下。在这个项目中用的是字典)方法,键值分别对应字符串和函数。另外,我们还需要一个指令指针,用来指向代码中下一个需要被执行的模块。
class Machine:
def __init__(self, code):
"""预先定义一个初始化函数""" self.data_stack = Stack()
self.return_addr_stack = Stack()
self.code = code
self.instruction_pointer = 0
再创建一些栈结构中必备的函数:
def pop(self):
return self.data_stack.pop() def push(self, value):
self.data_stack.push(value) def top(self):
return self.data_stack.top()
为了执行我们“操作码”(实际上,并不是真正意义上的操作码,只是一种动态类型,但是你懂得~)我们需要建立一个'dispatch'函数。但是在这之前,我们需要创建一个解释器的循环:
def run(self):
"""代码运行的条件""" while self.instruction_pointer < len(self.code):
opcode = self.code[self.instruction_pointer]
self.instruction_pointer += 1
self.dispatch(opcode)
上面的代码原理很简单:获取下一个指令,指令指针自增1个然后基于操作码执行'dispatch'函数,下面是'dispatch'函数的定义(函数定义有点长,你们可以尝试改进一下):
def dispatch(self, op):
dispatch_map = {
"%": self.mod,
"*": self.mul,
"+": self.plus,
"-": self.minus,
"/": self.div,
"==": self.eq,
"cast_int": self.cast_int,
"cast_str": self.cast_str,
"drop": self.drop,
"dup": self.dup,
"if": self.if_stmt,
"jmp": self.jmp,
"over": self.over,
"print": self.print_,
"println": self.println,
"read": self.read,
"stack": self.dump_stack,
"swap": self.swap,
} if op in dispatch_map:
dispatch_map[op]()
elif isinstance(op, int):
# 如果指令是整型数据,就将数据存放到数据栈中
self.push(op)
elif isinstance(op, str) and op[0]==op[-1]=='"':
# 如果是字符串类型的,就将字符串内容存放到数据栈中
self.push(op[1:-1])
else:
raise RuntimeError("Unknown opcode: '%s'" % op)
上面的代码非常浅显易懂,就是当输入一段指令,该函数就会根据这段指令在'dispatch_map'字典中找到对应的方法,比如:符号'\*'对应的是'self.mul'函数。以上过程就类似于'Forth'语言的构建过程。
当输入指令'*',程序就会执行函数'self.mul',所以我们还需要定义对应的函数:
def mul(self):
self.push(self.pop() * self.pop())
其他的函数定义也是依次类推,根据它们的功能和名称定义不同的函数。
到这里,你可以定义你想要的函数了,一个虚拟机环境基本构成就是这样!
然而并没有完,环境搭建好了,最重要的'解释'还没有完成,一个语言解释器包括两部分:
1. 解析:解析部分接受一个由字符序列表示的输入指令,然后将输入字符分解成一系列的词法单元
2. 执行:程序内部的解释器根据语义规则进一步处理词法单元,进而执行原指令的实际运算。
流程如下图所示:
下面一节中,我们将会讨论如何构建解析器。
2、为我们的指令创建一个简单的解析器
让我们使用'tokenize'模块为输入的指令构建一个解析器吧~
import tokenize
from StringIO import StringIO def parse(text): # 以StingIO的形式将text对象读入到内存中,并以字符串形式返回到 generate_tokens()函数中
tokens = tokenize.generate_tokens(StringIO(text).readline) # generate_tokens生成器生成一个5元祖:标记类型、标记字符串、标记开始位置二元组、标记结束位置二元组以及标记所在的行号
# 下面大写的单词都属于token模块的常量
for toknum, tokval, _, _, _ in tokens:
if toknum == tokenize.NUMBER:
yield int(tokval)
elif toknum in [tokenize.OP, tokenize.STRING, tokenize.NAME]:
yield tokval
elif toknum == tokenize.ENDMARKER:
break
else:
raise RuntimeError("Unknown token %s: '%s'" %
(tokenize.tok_name[toknum], tokval))
更多关于Python令牌器('tokenize')的常量查看请查阅官方文档。
3、简单优化:常量折叠
>“常量折叠”是 就是在编译器进行语法分析的时候,将常量表达式计算求值,并用求得的值来替换表达式,放入常量表。可以算作一种编译优化。
def constant_fold(code):
"""对简单的数学表达式诸如:2 3 + 进行计算并将结果作为常数返回原指令列表中"""
while True:
# 在指令中找到两个连续的数字以及一个算数运算符
for i, (a, b, op) in enumerate(zip(code, code[1:], code[2:])):
if isinstance(a, int) and isinstance(b, int) \
and op in {"+", "-", "*", "/"}:
m = Machine((a, b, op))
m.run()
code[i:i+3] = [m.top()]
print("Constant-folded %s%s%s to %s" % (a,op,b,m.top()))
break
else:
break
return code
这个方法唯一的缺点是我们必须得更新跳转的地址,尤其是在遇到读取或者跳转等操作时需要不断的跳转。但是任何问题都有它对应解决的方案,有一个简单的例子就是在跳转的时候只允许调到指令的命名标签上,这样的话,在执行常量折叠之后就可以跳转到它们真正的地址上。
我们可以通过以下代码实现一个简单的“读取-求值-输出循环”的交互式编程环境:
def repl():
print('Hit CTRL+D or type "exit" to quit.') while True:
try:
source = raw_input("> ")
code = list(parse(source))
code = constant_fold(code)
Machine(code).run()
except (RuntimeError, IndexError) as e:
print("IndexError: %s" % e)
except KeyboardInterrupt:
print("\nKeyboardInterrupt")
5、课后作业
1. 列表项试着在不查看完整源代码的情况下制作这个虚拟解释器(可以参考Python内置函数),并尝试生成'Fibonacci'序列,将运行过程和结果截图;
2. 如果完成了第一题,恭喜你,又'get'一个技能,你可以查看下面的完整代码对比你自己的代码,把你的代码中重要的细节和你的思考写入实验报告;
3. 那么接下来请尝试给指令中的函数添加'call'和'return'功能。提示:'call'函数是先将当前地址返回到栈中,再调用'self.jmp'函数。'return'函数显然是先将栈中的地址弹出,根据该地址设置一个指令指针从'call'函数中返回到原来开始被调用的地方。
6、完整代码
代码同样可以通过github获取:
#!/usr/bin/env python
# coding: utf-8 """
A simple VM interpreter. Code from the post at http://csl.name/post/vm/
This version should work on both Python 2 and 3.
""" from __future__ import print_function
from collections import deque
from io import StringIO
import sys
import tokenize def get_input(*args, **kw):
"""Read a string from standard input."""
if sys.version[0] == "":
return raw_input(*args, **kw)
else:
return input(*args, **kw) class Stack(deque):
push = deque.append def top(self):
return self[-1] class Machine:
def __init__(self, code):
self.data_stack = Stack()
self.return_stack = Stack()
self.instruction_pointer = 0
self.code = code def pop(self):
return self.data_stack.pop() def push(self, value):
self.data_stack.push(value) def top(self):
return self.data_stack.top() def run(self):
while self.instruction_pointer < len(self.code):
opcode = self.code[self.instruction_pointer]
self.instruction_pointer += 1
self.dispatch(opcode) def dispatch(self, op):
dispatch_map = {
"%": self.mod,
"*": self.mul,
"+": self.plus,
"-": self.minus,
"/": self.div,
"==": self.eq,
"cast_int": self.cast_int,
"cast_str": self.cast_str,
"drop": self.drop,
"dup": self.dup,
"exit": self.exit,
"if": self.if_stmt,
"jmp": self.jmp,
"over": self.over,
"print": self.print,
"println": self.println,
"read": self.read,
"stack": self.dump_stack,
"swap": self.swap,
} if op in dispatch_map:
dispatch_map[op]()
elif isinstance(op, int):
self.push(op) # push numbers on stack
elif isinstance(op, str) and op[0]==op[-1]=='"':
self.push(op[1:-1]) # push quoted strings on stack
else:
raise RuntimeError("Unknown opcode: '%s'" % op) # OPERATIONS FOLLOW: def plus(self):
self.push(self.pop() + self.pop()) def exit(self):
sys.exit(0) def minus(self):
last = self.pop()
self.push(self.pop() - last) def mul(self):
self.push(self.pop() * self.pop()) def div(self):
last = self.pop()
self.push(self.pop() / last) def mod(self):
last = self.pop()
self.push(self.pop() % last) def dup(self):
self.push(self.top()) def over(self):
b = self.pop()
a = self.pop()
self.push(a)
self.push(b)
self.push(a) def drop(self):
self.pop() def swap(self):
b = self.pop()
a = self.pop()
self.push(b)
self.push(a) def print(self):
sys.stdout.write(str(self.pop()))
sys.stdout.flush() def println(self):
sys.stdout.write("%s\n" % self.pop())
sys.stdout.flush() def read(self):
self.push(get_input()) def cast_int(self):
self.push(int(self.pop())) def cast_str(self):
self.push(str(self.pop())) def eq(self):
self.push(self.pop() == self.pop()) def if_stmt(self):
false_clause = self.pop()
true_clause = self.pop()
test = self.pop()
self.push(true_clause if test else false_clause) def jmp(self):
addr = self.pop()
if isinstance(addr, int) and 0 <= addr < len(self.code):
self.instruction_pointer = addr
else:
raise RuntimeError("JMP address must be a valid integer.") def dump_stack(self):
print("Data stack (top first):") for v in reversed(self.data_stack):
print(" - type %s, value '%s'" % (type(v), v)) def parse(text):
# Note that the tokenizer module is intended for parsing Python source
# code, so if you're going to expand on the parser, you may have to use
# another tokenizer. if sys.version[0] == "":
stream = StringIO(unicode(text))
else:
stream = StringIO(text) tokens = tokenize.generate_tokens(stream.readline) for toknum, tokval, _, _, _ in tokens:
if toknum == tokenize.NUMBER:
yield int(tokval)
elif toknum in [tokenize.OP, tokenize.STRING, tokenize.NAME]:
yield tokval
elif toknum == tokenize.ENDMARKER:
break
else:
raise RuntimeError("Unknown token %s: '%s'" %
(tokenize.tok_name[toknum], tokval)) def constant_fold(code):
"""Constant-folds simple mathematical expressions like 2 3 + to 5."""
while True:
# Find two consecutive numbers and an arithmetic operator
for i, (a, b, op) in enumerate(zip(code, code[1:], code[2:])):
if isinstance(a, int) and isinstance(b, int) \
and op in {"+", "-", "*", "/"}:
m = Machine((a, b, op))
m.run()
code[i:i+3] = [m.top()]
print("Constant-folded %s%s%s to %s" % (a,op,b,m.top()))
break
else:
break
return code def repl():
print('Hit CTRL+D or type "exit" to quit.') while True:
try:
source = get_input("> ")
code = list(parse(source))
code = constant_fold(code)
Machine(code).run()
except (RuntimeError, IndexError) as e:
print("IndexError: %s" % e)
except KeyboardInterrupt:
print("\nKeyboardInterrupt") def test(code = [2, 3, "+", 5, "*", "println"]):
print("Code before optimization: %s" % str(code))
optimized = constant_fold(code)
print("Code after optimization: %s" % str(optimized)) print("Stack after running original program:")
a = Machine(code)
a.run()
a.dump_stack() print("Stack after running optimized program:")
b = Machine(optimized)
b.run()
b.dump_stack() result = a.data_stack == b.data_stack
print("Result: %s" % ("OK" if result else "FAIL"))
return result def examples():
print("** Program 1: Runs the code for `print((2+3)*4)`")
Machine([2, 3, "+", 4, "*", "println"]).run() print("\n** Program 2: Ask for numbers, computes sum and product.")
Machine([
'"Enter a number: "', "print", "read", "cast_int",
'"Enter another number: "', "print", "read", "cast_int",
"over", "over",
'"Their sum is: "', "print", "+", "println",
'"Their product is: "', "print", "*", "println"
]).run() print("\n** Program 3: Shows branching and looping (use CTRL+D to exit).")
Machine([
'"Enter a number: "', "print", "read", "cast_int",
'"The number "', "print", "dup", "print", '" is "', "print",
2, "%", 0, "==", '"even."', '"odd."', "if", "println",
0, "jmp" # loop forever!
]).run() if __name__ == "__main__":
try:
if len(sys.argv) > 1:
cmd = sys.argv[1]
if cmd == "repl":
repl()
elif cmd == "test":
test()
examples()
else:
print("Commands: repl, test")
else:
repl()
except EOFError:
print("")
[译]Python编写虚拟解释器的更多相关文章
- 【译】使用 Python 编写虚拟机解释器
[译]如何使用 Python 创建一个虚拟机解释器? 原文地址:Making a simple VM interpreter in Python 更新:根据大家的评论我对代码做了轻微的改动.感谢 ro ...
- RobotFramework自动化测试框架-使用Python编写自定义的RobotFramework Lib
使用Python构建Lib工程 可以用来开发Python Lib的IDE工具有很多,常见的有Pycharm,Eclipse with PyDev插件等,而且在RobotFramework官网中也已经提 ...
- 【转】python编写规范——中标软件有限公司测试中心
[转]python编写规范 一.说明 二.内容 1. 代码布局 1.1 缩进 1.2 表达式和语句中的空格 1.3 行的最大长度 1.4 空行... 1.5 编码... 2. 语句... 2.1 标准 ...
- python编写shell脚本详细讲解
python编写shell脚本详细讲解 那,python可以做shell脚本吗? 首先介绍一个函数: os.system(command) 这个函数可以调用shell运行命令行command并且返回它 ...
- Python 编译器与解释器
Python 编译器与解释器 Python的环境我们已经搭建好了,可以开始学习基础知识了.但是,在此之前,还要先说说编译器与解释器相关的内容. 如果这部分内容,让你觉得难以理解或不能完全明白,可以暂时 ...
- 11 个最佳的 Python 编译器和解释器
原作:Archie Mistry 翻译:豌豆花下猫@Python猫 原文:https://morioh.com/p/765b19f066a4 Python 是一门对初学者友好的编程语言,是一种多用途的 ...
- python设计模式之解释器模式
python设计模式之解释器模式 对每个应用来说,至少有以下两种不同的用户分类. [ ] 基本用户:这类用户只希望能够凭直觉使用应用.他们不喜欢花太多时间配置或学习应用的内部.对他们来说,基本的用法就 ...
- 基于python编写的天气抓取程序
以前一直使用中国天气网的天气预报组件都挺好,可是自从他们升级组件后数据加载变得非常不稳定,因为JS的阻塞常常导致网站打开速度很慢.为了解决这个问题决定现学现用python编写一个抓取程序,每天定时抓取 ...
- 用Python编写博客导出工具
用Python编写博客导出工具 罗朝辉 (http://kesalin.github.io/) CC 许可,转载请注明出处 写在前面的话 我在 github 上用 octopress 搭建了个人博 ...
随机推荐
- TXT记事本转换PDF文件
使用的方式为,读取TXT记事本的内容,然后写入创建的PDF文件. static void Main(string[] args) { const string txtFile = "D:\\ ...
- 详解Winform里面的缓存使用
缓存在很多情况下需要用到,合理利用缓存可以一方面可以提高程序的响应速度,同时可以减少对特定资源访问的压力.本文主要针对自己在Winform方面的缓存使用做一个引导性的介绍,希望大家能够从中了解一些缓存 ...
- zabbix_server 已死,但是 subsys 被锁
一开始 grant all privileges on zabbix.* to zabbix@localhost identified by 'password'; 将localhost改为ip后恢 ...
- selenium如何随机选取省份和城市的下拉框的值
1.原始需求,选择省份后,相应的城市会自动加载 2.思路 a.获取下拉框的所有元素个数 b.随机点击0-元素个数之间的某个值 3.代码实现 Random random = new Random(); ...
- python如何控制数据库?
http://www.w3cschool.cc/python/python-mysql.html 通过利用MySQLdb可以操作数据库 实例: 以下实例链接Mysql的TESTDB数据库: # enc ...
- UESTC 916 方老师的分身III --拓扑排序
做法: 如果有a<b的关系,则连一条a->b的有向边,连好所有边后,找入度为0的点作为起点,将其赋为最小的价值888,然后其所有能到的端点,价值加1,加入队列,删去上一个点,然后循环往复, ...
- POJ 1703 Find them,Catch them ----种类并查集(经典)
http://blog.csdn.net/freezhanacmore/article/details/8774033?reload 这篇讲解非常好,我也是受这篇文章的启发才做出来的. 代码: #i ...
- 使用Unity3D的50个技巧:Unity3D最佳实践
翻译故事 原文:http://devmag.org.za/2012/07/12/50-tips-for-working-with-unity-best-practices/ 这篇技巧,我自己也在翻译, ...
- Unity手机平台播放影片
播放视频方法 截止到目前的Unity4.2版本,要在手机平台上播放影片,有两种方法: 使用Unity自带的Move Texture http://docs.unity3d.com/Documentat ...
- java10-1 Object类
Object:类 Object 是类层次结构的根类.每个类都使用 Object 作为超类. 每个类都直接或者间接的继承自Object类. Object类的方法: public int has ...