Python编译/解释过程
Posted
技术标签:
【中文标题】Python编译/解释过程【英文标题】:Python Compilation/Interpretation Process 【发布时间】:2011-03-18 23:56:53 【问题描述】:我正在尝试更清楚地了解 python 编译器/解释器过程。不幸的是,我没有上过口译课,也没有读过很多关于口译的文章。
基本上,我现在理解的是,来自.py
文件的 Python 代码首先被编译成 python 字节码(我假设这是我偶尔看到的 .pyc
文件?)。接下来,字节码被编译成机器码,一种处理器真正理解的语言。
差不多,我读过这个帖子Why python compile the source to bytecode before interpreting?
请记住,我对编译器/解释器的了解几乎不存在,有人能给我一个很好的解释吗?或者,如果这不可能,也许可以给我一些资源,让我快速了解编译器/解释器?
谢谢
【问题讨论】:
你不会“解释成机器代码”——这就是编译器所做的。 Python 解释器只执行字节码。 (它是字节码的 .pyc。) 在旁注中,知道原始 .py 文件的最后修改时间在 .pyc 文件中编码可能会有所帮助。这允许 Python 确定是否需要创建新的 .pyc 文件。 .pyc 文件的目的当然是避免每次调用脚本时解析整个脚本。如果使用 .pyc,Python 程序将不会运行得更快。只有加载时间会发生变化。 【参考方案1】:字节码实际上并没有被解释为机器码,除非你使用了一些特殊的实现,比如 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(传递给fib
)并将其推送到解释器的堆栈(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 加载到堆栈中。
【讨论】:
解释器/VM 是用 C 语言编写的。它是(有点过于简单化)一个循环,它使用当前字节在巨大的 switch 语句中选择许多情况之一。在开关中间的某个地方,有一个case LOAD_FAST:
,后面跟着一个读取接下来两个字节的代码,在一些“参数”集合中查找指定的参数,并将其推送到堆栈对象上。为了与外界交互,Python 允许调用扩展模块,这些扩展模块的作用类似于 Python 代码和对象,但实际上是编译代码,因此可以代表您的脚本直接与显卡等进行对话。
对你的最后一个问题更明确一点:没有用于“与显卡对话”的 Python 操作码。有一个“在这个模块中调用这个函数”的操作码,如果该模块是一个图形编程扩展模块,解释器将调用库的入口点来调用所请求的函数,并传递一些参数。 C 库(假设它是 C)梳理出参数,将它们从 Python 对象转换为 C 值和结构,并将调用转发到真正的图形库,然后在屏幕上弹出一个彩色三角形,或其他任何东西。
@Moondra CPython 从不将字节码转换为 switch case。我链接到的 C 代码是编译成机器代码的代码。该机器代码是 C 代码的 CPU 就绪表示。您应该将 C 代码和机器代码视为完全相同事物的不同表示。 C是人类可读的形式,而机器代码是机器可读的形式。需要理解的一个关键点是,C 程序(以其编译后的机器代码形式)是 CPU 将其视为代码的唯一东西。
... 相比之下,Python 字节码被 CPU 视为只是数据。 C 代码将数据解释为要执行的代码,因此得名解释器。
将 Python 字节码视为烹饪食谱可能会有所帮助,而将 C 代码视为烹饪机器人,它读取并遵循食谱以烹饪食物。机器人内部有代码,很可能是 C 代码,而食谱只是通过机器人的眼睛读取的数据,以便知道如何执行特定的烹饪程序。在一个层面上,配方就是代码——一组要遵循的指令。在另一个层面上,它只是输入机器人大脑的数据。 hth【参考方案2】:
为了完成伟大的Marcelo Cantos's answer,这里只是一个小的逐列摘要来解释反汇编字节码的输出。
例如,给定这个函数:
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 | |
每一列都有特定的用途:
-
源码中对应的行号
可选地指示执行的当前指令(例如,当字节码来自frame object 时)
一个标签,表示可能的
JUMP
从较早的指令到这个
字节码中对应字节索引的地址(这些是 2 的倍数,因为 Python 3.6 对每条指令使用 2 个字节,而在以前的版本中可能会有所不同)
指令名(也叫opname),每一条在the dis
module都有简单的解释,具体实现见ceval.c
(CPython的核心循环)
指令的参数(如果有)(如果有),Python 内部使用它来获取一些常量或变量、管理堆栈、跳转到特定指令等。
指令参数的人性化解释
【讨论】:
如何从中读取执行速度?没有时间。 @YumiTada “执行速度”是什么意思?这只是编译的字节码,尚未(尚未)执行,因此这里的时间无关紧要。以上是关于Python编译/解释过程的主要内容,如果未能解决你的问题,请参考以下文章