Python 是一种解释型语言,这意味着它的执行方式与编译型语言如 C 或 Java 有所不同。但这并不意味着 Python 没有编译过程。事实上,Python 先将源代码编译为字节码,然后解释或执行这些字节码。以下是 Python 的编译和执行过程的详细步骤:
-
源代码 (
.py
文件):
你的 Python 程序开始作为一个简单的文本文件,其中包含 Python 源代码。 -
编译过程:
当你尝试运行 Python 程序时,Python 编译器会首先将其转换为 Python 字节码。这一步是由 Python 的内部编译器PyCompiler
完成的。 -
字节码 (
.pyc
文件):
字节码是一种低级、与平台无关的表示形式,它更接近于机器代码。字节码文件被存储在__pycache__
目录下,通常为.pyc
扩展名。这些字节码可以被 Python 解释器直接使用,从而快速执行,而不需要重新从源代码编译。值得注意的是,
.pyc
文件并不是严格必要的。它只是加速了程序的启动,因为重新从源代码生成字节码需要时间。 -
Python 虚拟机 (PVM):
一旦生成了字节码,Python 虚拟机 (PVM, Python Virtual Machine) 便开始解释并执行这些字节码。PVM 通常不是一个独立的程序,而是 Python 解释器的一部分。它负责执行编译后的字节码。 -
执行:
在 PVM 内,字节码被一条一条地执行。这意味着字节码被逐条解释并执行,而不是被翻译成机器代码。
总结,Python 的运行流程包括两个主要阶段:编译阶段和解释阶段。在编译阶段,源代码被转换为字节码;在解释阶段,字节码在 Python 虚拟机中被执行。这种方式结合了编译型语言的一些优点(如预处理的错误检查和某些优化)和解释型语言的灵活性与跨平台能力。
Python 字节码
为了了解 Python 字节码,首先我们可以创建一个简单的 Python 函数,并使用内置的 dis
模块来查看其字节码表示。以下是如何做到这一点的示例。
首先,让我们定义一个简单的 Python 函数:
def add(a, b):result = a + breturn result
接着,我们使用 dis
模块来反汇编该函数,以查看其字节码表示:
import disdis.dis(add)
执行上述代码可能会产生以下输出(取决于你使用的 Python 版本):
2 0 LOAD_FAST 0 (a)2 LOAD_FAST 1 (b)4 BINARY_ADD6 STORE_FAST 2 (result)3 8 LOAD_FAST 2 (result)10 RETURN_VALUE
这里是对字节码的解读:
LOAD_FAST
:将一个局部变量(例如a
或b
)加载到堆栈上。0 (a)
表示局部变量的索引,以及该变量的名称。BINARY_ADD
:从堆栈中弹出两个值,将它们相加,然后将结果压回堆栈。STORE_FAST
:将堆栈顶部的值存储到局部变量中(这里是result
)。RETURN_VALUE
:从堆栈中弹出一个值并将其返回。
① 每个指令通常都有一个与之关联的操作数,这在这里是括号中的值(例如 0 (a)
)。此值为指令提供额外的上下文,例如要加载或存储的特定变量。
② 在这个字节码输出中,前面的 2
和 3
是源代码的行号。这些行号对应于 Python 源代码中生成这些特定字节码的位置。
这样的输出设计有助于开发者理解哪一行源代码对应于哪些字节码指令,从而在调试或分析时更容易定位问题。
在提供的字节码中:
- 行号
2
对应于源代码中定义result
的行,即result = a + b
。 - 行号
3
对应于源代码中的返回语句,即return result
。
总之,这些行号是为了帮助你将字节码指令与其在原始 Python 源代码中的位置联系起来。
③ 前面的 0
, 2
, 4
, 6
, 8
, 10
是字节码指令的偏移量(offsets)。在字节码序列中,每个指令都有一个相应的偏移量,表示该指令从字节码序列开始的字节距离。
为什么有些偏移量会跳过一个数字,如从 0
到 2
而不是 1
?这是因为不同的字节码指令以及它们的参数可能需要不同数量的字节。LOAD_FAST
指令本身占用一个字节,它的参数(例如 0 (a)
)占用另一个字节,总共两个字节。因此,LOAD_FAST 0 (a)
占用两个字节,从偏移量 0
开始,所以下一个指令的偏移量是 2
。
这些偏移量在调试、跟踪和分析字节码时很有用,尤其是当你需要理解控制流如何跳转到特定的指令时(例如在循环或条件分支中)。
这个例子为我们提供了一个简单的 Python 函数的字节码表示的概览。实际上,Python 字节码包含许多其他指令,它们用于实现更复杂的操作和控制流结构。
Python 虚拟机
Python 虚拟机(PVM,Python Virtual Machine)是一个抽象的计算机,它作为 Python 解释器的核心组件,负责执行 Python 字节码。PVM 实际上并不是一个独立的程序或服务,而是 Python 解释器(例如 CPython)的一部分。当我们谈论“运行 Python 代码”时,我们实际上是在谈论 PVM 解释并执行字节码。
以下是 PVM 的关键特点和详细介绍:
-
字节码解释器:PVM 的主要职责是读取并执行字节码。字节码是 Python 源代码经过编译后的低级、平台无关的表示。
-
平台独立性:由于 PVM 解释的是字节码而不是直接执行机器代码,因此 Python 代码可以轻松地在任何平台上运行,只要该平台上有合适的解释器和 PVM。
-
运行时环境:PVM 提供了运行时所需的所有资源,如内存管理(包括垃圾回收)、系统调用接口以及许多内置函数和类。
-
性能考虑:纯解释的字节码通常比原生机器代码运行得慢,因为需要一步步解释和执行,而不是直接在硬件上执行。但是,由于大量的优化技术和 JIT(即时编译)技术的应用,Python 的某些实现(如 PyPy)可以显著提高执行速度。
-
内存管理:PVM 内部包括一个内存分配器和垃圾回收器。这确保了对象在不再使用时可以被正确地清理,同时还处理了循环引用等复杂情况。
-
扩展性:虽然 PVM 主要是用 C 语言编写的(在 CPython 的情况下),但它可以与其他语言编写的代码进行互操作,例如 C、C++ 或 Fortran。这使得 Python 可以利用其他语言的高性能库。
-
CPython vs 其他实现:当我们提到 PVM 时,我们通常指的是 CPython(Python 的官方实现)的虚拟机。但还有其他的 Python 实现,如 Jython(运行在 Java 虚拟机上)、IronPython(针对 .NET CLR)和 PyPy(带有 JIT 编译器的 Python)。这些实现都有自己的虚拟机和特性。
总之,Python 虚拟机是执行 Python 代码的引擎,它解释并运行字节码,提供了与操作系统和硬件之间的抽象层,确保了 Python 的跨平台特性和灵活性。