Python 编译/解释过程
我想更清楚地理解Python的编译器/解释器过程。可惜我没有上过相关的课程,也没读过太多这方面的内容。
基本上,我现在理解的是,Python代码来自.py
文件,首先会被编译成Python字节码(我猜这就是我偶尔看到的.pyc
文件?)。接下来,这些字节码会被进一步编译成机器代码,也就是处理器能理解的语言。大致上,我读过这个帖子 为什么Python在解释之前要将源代码编译成字节码?
有没有人能给我一个简单明了的解释,讲讲整个过程,考虑到我对编译器/解释器几乎没有了解?或者,如果不行的话,能不能给我一些资源,让我快速了解编译器/解释器的概念?
谢谢
2 个回答
为了补充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 | |
每一列都有特定的用途:
字节码其实并不是直接转化为机器码,除非你使用一些特别的实现,比如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执行;也不会被转化成CPU能理解的东西(至少在官方的Python C实现中是这样的)。CPU执行的是虚拟机的代码,这些代码完成字节码指令所指示的工作。当解释器想要执行fib
函数时,它会一条一条地读取指令,并按照指令的要求去做。比如,它首先看到的指令是LOAD_FAST 0
,这意味着它会从存放参数的地方取出参数0(也就是传给fib
的n
),然后把这个值放到解释器的栈上(Python的解释器是一个栈机器)。接下来,读取到的指令是LOAD_CONST 1
,这时它会从函数拥有的常量集合中取出常量1,这里恰好是数字2,然后把它也放到栈上。你实际上可以看到这些常量:
>>> fib.func_code.co_consts
(None, 2, 1)
接下来的指令COMPARE_OP 0
,指示解释器将栈顶的两个元素弹出,并进行不等式比较,然后把比较结果(布尔值)再放回栈上。第四条指令根据这个布尔值来决定是跳过五条指令还是继续执行下一条指令。这些内容解释了fib
中的条件表达式if n < 2
。你可以尝试理解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加载到栈上。