Python Compilation/Interpretation Process
我试图更清楚地理解python编译器/解释器进程。 不幸的是,我没有参加口译课,也没有读过很多关于它们的内容。
基本上,我现在理解的是.py文件中的Python代码首先编译成python字节码(我假设偶尔会看到.pyc文件?)。 接下来,字节码被编译成机器代码,这是处理器实际理解的语言。
差不多,我已经读过这个帖子为什么python在解释前将源代码编译成字节码?
有人能给我一个很好的解释整个过程,记住我对编译器/解释器的了解几乎不存在吗? 或者,如果这不可能,也许给我一些资源,快速概述编译器/解释器?
谢谢
-
你没有"解释成机器代码" - 这就是编译器的作用。 Python解释器只执行字节码。 (对于字节码,它是.pyc。)
-
另外,您可能会发现原始.py文件的最后修改时间是在.pyc文件中编码的。 这允许Python确定是否需要创建新的.pyc文件。 当然,.pyc文件的目的是避免在每次调用脚本时解析整个脚本。 如果使用.pyc,Python程序将无法运行得更快。 只有加载时间会发生变化。
字节码实际上并不解释为机器代码,除非你使用一些奇特的实现,如pypy。
除此之外,您的描述是正确的。字节码被加载到Python运行时并由虚拟机解释,虚拟机是一段代码,它读取字节码中的每条指令并执行指示的任何操作。您可以使用dis模块查看此字节码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| >>> 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,并将其推送到堆栈中。你实际上可以看到这些常量:
1 2
| >>> fib.func_code.co_consts
(None, 2, 1) |
下一条指令COMPARE_OP 0告诉解释器弹出两个最顶层的堆栈元素并在它们之间执行不等式比较,将布尔结果推回堆栈。第四条指令根据布尔值确定是跳过五条指令还是继续执行下一条指令。所有这些措辞都解释了fib中条件表达式的if n < 2部分。对于您来说,弄清楚fib字节码的其余部分的含义和行为将是一个非常有益的练习。唯一一个,我不确定是POP_TOP;我猜测JUMP_IF_FALSE被定义为将其布尔参数留在堆栈上而不是弹出它,因此必须明确地弹出它。
更有启发性的是检查fib的原始字节码,从而:
1 2 3 4 5 6 7 8 9 10 11 12 13
| >>> 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加载到堆栈上。
-
所以,如果它没有变成机器代码......我的x86处理器最终如何执行?我的印象是,我的计算机上发生的一切最终都可以分解为我的处理器或其他硬件正在读取的1和0。
-
@JGord:我已经扩展了我的答案,以解决你的评论。
-
好的,那么我脑海中浮现的部分是我的处理器不理解LOAD_FAST操作码是否正确?这是python虚拟机的字节码。所以不知何故,虚拟机是用一种可以组装成x86的语言编写的。当然。但是,虚拟机如何实际执行它从我的硬件上的字节代码解释的操作?我们来举个例子吧。我的脚本做了一些计算,它需要通过总线将结果发送到我的显卡。 python虚拟机无法做到这一点,对吗?不知何故,物理过程正在做什么?
-
解释器/ VM在C中。它(稍微过分简化)一个循环,它使用当前字节来选择巨大的switch语句中的许多情况之一。在交换机中间的某处,有一个case LOAD_FAST:,后面跟着读取下两个字节的代码,查找某些"parameters"集合中的指定参数,并将其推送到堆栈对象上。为了与外界进行交互,Python允许调用扩展模块,扩展模块的行为类似于Python代码和对象,但它们实际上是编译代码,因此可以直接代表您的脚本与图形卡等进行通信。
-
为了更清楚一点你的上一个问题:没有用于"与显卡通信"的Python操作码。有一个"在此模块中调用此函数"的操作码,如果模块是图形编程扩展模块,则解释器将调用库的入口点以获取所请求的函数,并向其传递一些参数。 C库(假设它是C)梳理出参数,将它们从Python对象转换为C值和结构,然后将调用转发到真正的图形库,然后在屏幕上或其他任何地方插入彩色三角形。
-
哇,谢谢马塞洛!这正是我想要的。介意告诉我你是如何/在哪里学到的?另外,你确定C库是参数被"挑出"的地方吗?这对我来说似乎违反直觉。为什么不理解python字节码的python解释器(因此我假设理解python对象)会梳理出参数并将C变量作为参数发送?
-
空闲的好奇心......差不多三十年了(当然不是全部的Python)。 Python解释器不知道任意C库所需的C数据类型。所以它基本上将它自己的Python对象参数的内部表示传递给库,它可以用它们做它想要的东西。在这里阅读一下,了解它是如何工作的。
-
嗨Marcelo,尽管阅读了你的评论和帖子,但我仍然对CPU如何执行Python字节码指令感到困惑。 VM可以读取Python字节码并执行指令,但最终要让VM执行指令,他们需要将这些指令发送到CPU(对吗?)。那么是不是有将这些指令编译成CPU可读指令的阶段?谢谢。
-
@Moondra VM本身就是一段本机代码 - 用C语言编写并编译成机器代码 - 完成实际工作。它遍历字节码并执行它在其中找到的操作。重新访问我之前给出的LOAD_FAST示例,代码可以在github.com/python/cpython/blob/3.6/Python/ceval.c#L1274找到。它相当嘈杂,但它的内容是GETLOCAL(oparg),它将对应于操作码参数的参数提取到本地C变量中,而PUSH(value)则将参数提取到Python堆栈上。也就是说,基本上是LOAD_FAST。
-
@MarceloCantos感谢您的回复。我看过一些字节码和CPython代码,但我只有Python经验,没有C经验,这就是为什么我遇到这么多麻烦。"VM本身就是一段本机代码 - 用C语言编写并编译成机器代码 - 完成实际工作" - 你的意思是所有的VM交换机案例都预编译为机器语言吗?因此,当VM选择一个开关盒并执行它时,它实际上直接与CPU的内存通信?因此不需要C再次编译。
-
到目前为止,我理解字节码被转换为VM执行的switch case。从本质上讲,开关案例是字节码指令的表示,但只是用C语言编写。但由于我根本不理解C,(遗憾的是还有很多东西要学习Python),我不明白当你发生什么时会发生什么在交换机中执行C代码。我已经读过C代码首先需要在执行之前编译成机器代码。这是我们在执行开关案例时在这里做的吗?
-
或者是所有内容都预先编译为机器代码,因此切换案例的执行会立即产生结果。非常感谢你的耐心等待。
-
@Moondra CPython从不将字节码转换为切换案例。我链接的C代码是编译成机器代码的代码。该机器代码是C代码的CPU就绪表示。您应该将C代码和机器代码视为完全相同的不同表示。 C是人类可读的形式,而机器代码是机器可读的形式。要理解的一个关键点是C程序(以其编译的机器代码形式)是CPU看到的唯一代码。
-
相反,Python字节码被CPU视为数据。 C代码将该数据解释为要执行的代码,因此将名称解释器解释。
-
可能有助于将Python字节码视为烹饪配方,将C代码视为烹饪机器人,其读取和遵循食谱以烹饪食物。机器人本身内部有代码,很可能是C代码,而食谱只是通过机器人的眼睛读取的数据,以便知道如何执行特定的烹饪程序。在一个层面上,配方是代码 - 一组要遵循的指令。在另一个层面上,它只是被送到机器人大脑的数据。心连心
-
啊,对不起'翻译'是一个糟糕的选择。我打算使用解释。所以最终C代码被编译成机器代码!这就是令人困惑的地方。现在,据我所知,Python代码可以逐行解释 - 当我使用解释器时,我可以实时运行行,但我们不能用C做对吗?根据我的理解,C必须一次性将整个代码编译成机器语言。在将其编译为机器代码之前,它必须再次解释整个字节码(过去的行和新的行)。
-
所以每次我们通过解释器运行Python代码时,我们是否强迫C再次重新编译整个字节码(新行和旧行)?
-
@Moondra C代码编译一次以制作Python解释器(Windows上的python.exe,大多数其他平台上的python)。那就是"烹饪"机器人。然后运行解释器,将Python代码传递给它。解释器读取代码并将其"编译"为字节码,然后解释。每次运行该代码时都会这样做。
-
对导入的模块进行了优化。编译导入的.py文件后,解释器将字节码缓存为导入的.py文件旁边的.pyc文件。无论何时导入模块,解释器首先检查是否存在比.py文件更新的相应.pyc文件。如果有,则直接加载.pyc文件,而不是重新编译.py文件。
-
@MarceloCantos如果C代码只需要编译一次(运行解释器),我们可以通过解释器多次运行代码 - 解释器似乎本质上是一个专门用于读取Python字节码的虚拟CPU。我猜这就是为什么它被称为虚拟机。我想我明白了。非常感谢你的耐心等待。这些细节很难在网上找到,初学者可以理解。再次非常感谢你。
-
@Moondra那就是现场!乐意效劳。
-
嗨,@ MarceloCantos感谢您的精彩解释!虽然函数fib的co_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',但Python解释器无需导入模块opcode然后将|转换为LOAD_FAST然后执行某些操作,对吧?它只是在读取|时进行操作,模块opcode只是帮助人们不知道co_code的功能,对吧?
-
@roachsinai说的没错。
-
@MarceloCantos感谢您的回复,我已经问了一个相关问题,链接是stackoverflow.com/questions/55843979/… 。希望你有时间回答它。提前致谢。
为了完成伟大的Marcelo Cantos的答案,这里只是一个小的逐列摘要来解释反汇编字节码的输出。
例如,给定此功能:
1 2 3 4
| def f(num):
if num == 42:
return True
return False |
这可能会被反汇编成(Python 3.6):
1 2 3 4 5 6 7 8 9 10 11 12
| (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 | | |
每列都有特定目的:
源代码中对应的行号
(可选)指示当前执行的指令(例如,当字节码来自帧对象时)
一个标签,表示从之前的指令到此可能的JUMP
字节码中对应于字节索引的地址(那些是2的倍数,因为Python 3.6对每条指令使用2个字节,而在以前的版本中可能会有所不同)
指令名称(也称为opname),每个都在dis模块中简要解释,它们的实现可以在ceval.c(CPython的核心循环)中找到
Python内部用于获取某些常量或变量,管理堆栈,跳转到特定指令等的指令的参数(如果有)。
教学论证的人性化解释
-
如何从中读取实现速度?没有时间。
-
@YumiTada"执行速度"是什么意思?这只是编译后的字节码而不是(尚未)执行,因此这里的时序无关紧要。
-
非常好的答案教程