简单的理解python解释器的运行机制

简单理解Python解释器


Python常见的解释器

  1. CPython
    该解释器是Python官方的解释器。使用C语言开发。在命令行中直接使用Python Shell就是使用的CPython解释器。是使用最广泛的解释器
  2. IPython
    IPython是基于CPython的一个交互式解释器。只是在交互方式上进行了改进。底层运行还是CPython。IPython使用 ‘ In [序号]: ’ 作为提示符。是不是很熟悉。安装Anaconda后,附带的Jupyter NoteBook就是使用此解释器。
  3. PyPy
    PyPy以运行速度为目的。显著的提高了执行速度。与CPython有所不同。所以有可能产生错误。
  4. Jython
    运行在Java平台的Python解释器。将代码编译成Java字节码运行。
  5. 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)

实际的字节码

  1. 使用__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]
  1. 使用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                      
>>>                                               

  1. 通过以上的内容我们已经知道了一个函数内的字节码是如何工作的。那函数之间的呢。这里我们引入一个概念:“帧”。帧包含了一端代码运行时所需的信息与上下文环境。在代码执行时动态的创建与销毁。每一个帧对应一次函数的调用。因为一个函数可以递归调用自己多次,所以一个code object可以拥有多个帧。
  2. 解释器中常用的两种栈,一种是数据栈,执行字节码操作时使用,上文已经涉及。还有一种叫块栈,用于特定的控制流(循环,异常处理),每一个帧都有自己的数据栈和块栈。
  3. 在多个函数之间每个函数运行开始时,将函数对应的帧压入栈中(函数的调用栈),结束时将对应的帧弹出,并将return value的值压入下一个帧的数据栈中。就完成了一次函数间的值传递。

总结一哈

python解释器包含常用的三种栈,调用栈,数据栈,块栈。

  • 调用栈的运行
    解释器首先将源码编译为字节码,创建调用栈,以创建第一个帧开始运行,在这之后不断的新建帧,或弹出帧并拿到返回值,调用栈的长度随之变化,直到第一个创建帧返回值,运行结束。

  • 帧的运行

  1. 每一个帧都有对应的code_object,一条没有参数的指令占据一个字节,有参数的指令占据三个字节,后两个字节为参数。这里的参数因指令的不同而效果不同,比如指令POP_JUMP_IF_FALSE,它的参数指的是跳转目标。BUILD_LIST, 它的参数是列表的个数。LOAD_CONST,它的参数是常量的索引。
  2. 在一个帧运行时,在一个循环中顺序执行每一条指令(使用指令名和参数完成具体操作)。一个标记量标记当前运行到字节码的哪个位置。直到捕获到异常或运行完所有指令拿到返回值。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 我可以提供一些有关如何使用Python编写Python解释器的指导:首先,你需要学习Python语言的基础知识,然后掌握Python代码的执行流程,接下来,你可以参考相关资料,学习如何设计和实现Python解释器,最后,你可以使用Python来实现自己的Python解释器。 ### 回答2: 要用Python编写一个Python解释器,可以采取以下步骤: 1. 设计解释器的基本框架:确定解释器的组成部分,包括词法分析器、语法分析器、语义分析器和执行器等。 2. 实现词法分析器:该部分负责将输入的代码转化成分词的形式,将源代码转化为一系列的标记(token),例如将关键字、标识符、操作符等转化为语法分析器可以理解的形式。 3. 实现语法分析器:该部分负责根据词法分析器的输出,使用语法规则进行语法分析,构建抽象语法树(AST),以便进一步的语义分析和执行。 4. 实现语义分析器:该部分负责对抽象语法树进行语义分析,检查代码的合法性和一致性。例如,检查变量的作用域、类型检查、函数定义和调用等。 5. 实现执行器:该部分负责基于语义分析器的输出,执行源代码并产生相应的结果。这可能需要实现解释器的运行时环境,包括变量和内存管理、控制流程等。 6. 编写测试用例:为了验证解释器的正确性,可以编写各种类型的测试用例,包括语法正确与错误的代码、边界情况和特殊情况的输入等。 7. 调试和优化:通过运行测试用例,发现解释器的错误和性能问题,并进行调试和优化,以确保解释器的正确性和效率。 编写一个完整的Python解释器是一项复杂的任务,需要深入理解Python的语法和语义,并具备编译器设计与实现的相关知识。然而,通过逐步实现词法分析器、语法分析器、语义分析器和执行器,并逐渐完善解释器的各个部分,可以逐渐实现一个简化版的Python解释器。 ### 回答3: 要用Python写一个Python解释器,需要理解解释器的基本原理和工作流程。 Python解释器的主要功能是将Python代码转换为可以被计算机执行的机器码。为了实现这个功能,可以使用Python自带的ast模块,它可以将Python代码解析成抽象语法树(AST)。通过分析AST,可以获得代码的结构和语义信息。 首先,需要编写一个词法分析器(Lexer),它将Python代码分解成一个个Token。Token是代码的最小语法单位,例如标识符、关键字、运算符等。然后,编写一个语法分析器(Parser),它将Token组织成语法树。语法树表示代码的结构和嵌套关系。 接下来,可以实现解释器的核心部分——执行器(Executor)。执行器遍历语法树,并根据语法树节点的类型进行相应的操作。例如,对于赋值语句节点,执行器将计算右侧表达式的值,并将结果存储到左侧标识符所对应的变量中。 在执行过程中,还需要处理函数和模块的导入、异常处理、作用域等问题。这些都需要在解释器中实现相应的机制。 最后,可以编写一个交互式界面,使得用户可以直接在命令行中输入Python代码,并查看执行结果。 总结起来,编写一个完整的Python解释器是一个庞大而复杂的任务,需要深入理解Python语言及其解释和执行机制。对于初学者来说,可以从简单的特定功能的解释器入手,逐步进行扩展和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值