52

我试图更清楚地理解 python 编译器/解释器过程。不幸的是,我没有上过口译课,也没有读过很多关于口译的文章。

基本上,我现在所理解的是,.py文件中的 Python 代码首先被编译成 python 字节码(我假设是.pyc我偶尔看到的文件?)。接下来,字节码被编译成机器码,一种处理器真正理解的语言。差不多,我读过这个线程Why python compile the source to bytecode before interpreting?

有人可以给我一个很好的解释整个过程,记住我对编译器/解释器的了解几乎不存在吗?或者,如果这不可能,也许可以给我一些资源来快速概述编译器/解释器?

谢谢

4

2 回答 2

63

字节码实际上并没有被解释为机器码,除非你使用了一些特殊的实现,比如 pypy。

除此之外,您的描述正确。字节码被加载到 Python 运行时并由虚拟机解释,虚拟机是一段代码,读取字节码中的每条指令并执行指示的任何操作。您可以使用模块查看此字节码dis,如下所示:

>>> def fib(n): return n if n < 2 else fib(n - 2) + fib(n - 1)
... 
>>> fib(10)
55
>>> import dis
>>> dis.dis(fib)
  1           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (2)
              6 COMPARE_OP               0 (<)
              9 JUMP_IF_FALSE            5 (to 17)
             12 POP_TOP             
             13 LOAD_FAST                0 (n)
             16 RETURN_VALUE        
        >>   17 POP_TOP             
             18 LOAD_GLOBAL              0 (fib)
             21 LOAD_FAST                0 (n)
             24 LOAD_CONST               1 (2)
             27 BINARY_SUBTRACT     
             28 CALL_FUNCTION            1
             31 LOAD_GLOBAL              0 (fib)
             34 LOAD_FAST                0 (n)
             37 LOAD_CONST               2 (1)
             40 BINARY_SUBTRACT     
             41 CALL_FUNCTION            1
             44 BINARY_ADD          
             45 RETURN_VALUE        
>>> 

详细解释

理解上面的代码永远不会被你的 CPU 执行是非常重要的。它也没有被转换成某种东西(至少,不是在 Python 的官方 C 实现中)。CPU 执行虚拟机代码,虚拟机代码执行字节码指令指示的工作。当解释器想要执行fib函数时,它会一次读取一条指令,然后按照指令执行的操作。它查看第一条指令 ,LOAD_FAST 0因此从保存参数的任何位置获取参数 0(n传递给fib)并将其推送到解释器的堆栈(Python 的解释器是堆栈机器)。在阅读下一条指令时,LOAD_CONST 1,它从函数拥有的常量集合中获取常量 1,在这种情况下恰好是数字 2,并将其压入堆栈。您实际上可以看到这些常量:

>>> fib.func_code.co_consts
(None, 2, 1)

下一条指令COMPARE_OP 0告诉解释器弹出两个最顶层的堆栈元素并在它们之间执行不等式比较,将布尔结果推回堆栈。第四条指令根据布尔值确定是向前跳转五条指令还是继续下一条指令。所有这些花言巧语都解释if n < 2fib. 这将是一个非常有指导意义的练习,可以让您梳理出其余fib字节码的含义和行为。唯一一个,我不确定是POP_TOP;我猜JUMP_IF_FALSE被定义为将其布尔参数留在堆栈上而不是弹出它,因此必须显式弹出它。

更有指导意义的是检查原始字节码fib

>>> code = fib.func_code.co_code
>>> code
'|\x00\x00d\x01\x00j\x00\x00o\x05\x00\x01|\x00\x00S\x01t\x00\x00|\x00\x00d\x01\x00\x18\x83\x01\x00t\x00\x00|\x00\x00d\x02\x00\x18\x83\x01\x00\x17S'
>>> import opcode
>>> op = code[0]
>>> op
'|'
>>> op = ord(op)
>>> op
124
>>> opcode.opname[op]
'LOAD_FAST'
>>> 

因此,您可以看到字节码的第一个字节是LOAD_FAST指令。下一对字节'\x00\x00'(16 位中的数字 0)是 的参数LOAD_FAST,并告诉字节码解释器将参数 0 加载到堆栈中。

于 2010-07-21T13:28:54.747 回答
5

为了完成伟大的Marcelo Cantos 的回答,这里只是一个小的逐列摘要来解释反汇编字节码的输出。

例如,给定这个函数:

def f(num):
    if num == 42:
        return True
    return False

这可以反汇编成(Python 3.6):

(1)|(2)|(3)|(4)|          (5)         |(6)|  (7)
---|---|---|---|----------------------|---|-------
  2|   |   |  0|LOAD_FAST             |  0|(num)
   |-->|   |  2|LOAD_CONST            |  1|(42)
   |   |   |  4|COMPARE_OP            |  2|(==)
   |   |   |  6|POP_JUMP_IF_FALSE     | 12|
   |   |   |   |                      |   |
  3|   |   |  8|LOAD_CONST            |  2|(True)
   |   |   | 10|RETURN_VALUE          |   |
   |   |   |   |                      |   |
  4|   |>> | 12|LOAD_CONST            |  3|(False)
   |   |   | 14|RETURN_VALUE          |   |

每列都有特定的用途:

  1. 源码中对应的行号
  2. 可选地指示当前执行的指令(例如,当字节码来自帧对象时)
  3. 一个标签,表示JUMP从较早的指令到本指令的可能
  4. 字节码中对应于字节索引的地址(这些是 2 的倍数,因为 Python 3.6 每条指令使用 2 个字节,而在以前的版本中可能会有所不同)
  5. 指令名称(也称为opname),每一个在模块中都有简单的解释,它们dis实现可以在ceval.c(CPython的核心循环)中找到
  6. Python内部使用的指令的参数(如果有)来获取一些常量或变量,管理堆栈,跳转到特定指令等。
  7. 指令参数的人性化解释
于 2017-11-28T10:19:00.703 回答