【Python】浅谈 字节码 & 虚拟机 (Python 解释器)

目录

一、绪论

二、说明

2.1 字节码编译

2.2 Python 虚拟机 (PVM)

2.3 性能意义 ☆

2.4 开发意义

三、小结


一、绪论

Python 通常被描述为一种解释语言,在这类语言中,源代码在程序运行时被“翻译”成“指令”,但这还不确切。同许多解释型语言一样,Python 在正式处理代码前,内部先进行预处理,将 Python 源代码编译成字节码,然后将其转发至 Python 虚拟机中。换言之,Python 实际上将源代码编译为虚拟机的一组指令,而这种中间格式的指令称为“字节码”。

二、说明

2.1 字节码编译

执行 Python 程序时,Python 内部将隐式地将 源代码 (source code) 编译成 字节码 (byte code)。编译 (compile) 可以理解为一种简单的“翻译”步骤 (类比于 C、C++ 等静态语言的编译,但结果有别);字节码则是一种 低级的与平台无关的 代码表现形式,有别于机器的二进制代码 (如 Intel 或 ARM 芯片的指令)。简言之,Python 将源代码语句的每一条分解为单一步骤,从而“翻译”为一组字节码指令。相比于文本文件中的 Python 源代码,字节码的运行速度更快。

注意,上述过程对大多数用户而言是隐式的。若 Python 进程在机器上拥有写入权限, 那么它将把程序的字节码保存为一个后缀为 .pyc 的文件 (即已编译的 .py (compiled)) 。在 Python3 中,.pyc 字节码文件被存储在名为 __pycache__ 的子目录中,该子目录位于与 .py 源文件相同的路径下。而 __pycache__ 子目录中的文件命名中包含了编译它们的 Python 的版本信息 (基于创建它的特定 Python 进制代码版本,如 test.cpython-36.pyc) 。例如:

在 Windows 下创建一个 test.py 文件:

然后在 IDE 中编译 (后续说明原因),在 test.py 所在目录下可以显式看到多出了一个 __pycache__ 子目录:

进入 __pycache__ 子目录中可以看到其字节码文件  test.cpython-36.pyc:

在记事本中打开,可见由于编码问题无法直接阅读,但可以看到 test.py 的程序主体也在其中:

事实上,当一个模块首次被导入或修改已编译的源文件时,都会在 .py 所在目录的 __pycache__ 的子目录下创建一个包含已编译代码的 .pyc 文件。新创建的 __pycache__ 子目录能够 避免太多文件挤在同一路径下,而新的字节码文件命名规范确保了同一主机上安装的不同版本的 Python 所生成的字节码文件 不会相互覆盖。当然,这些字节码文件都是自动生成的,与大多数 Python 程序无关,也随着不同版本的 Python 有着不同的形式。

上述 Python 保存字节码的方式是对 Python 程序 启动速度 的一种优化 (而非运行速度的优化)。下次运行程序时,若上次保存字节码后未再修改过源代码,并且使用同一个 Python 编译器版本运行, 那么 Python 将会加载 .pyc 文件并跳过编译步骤。该过程工作原理如下:

  • 源文件的改变:Python 会自动检查源文件和字节码文件最后一次修改的时间戳,以确认是否必须重新编译:若源代码被编辑并保存,则下次程序运行时,将自动重新创建字节码文件。
  • Python 的版本:导入 (import) 机制会检查是否需要因使用了不同的 Python 版本而重新编译,这些版本信息在 Python3 中存储于字节码文件名中间部分。

可见,源文件的修改和 Python 版本的改变都会触发新的字节码文件的编译。导入模块时,若同时存在 .py 和 .pyc 文件,Python 将优先使用 .pyc 文件运行;若 .pyc 文件的编译时间早于 .py 的时间,则将重新编译 .py 并更新 .pyc 文件。还有,字节码文件也是发布 Python 程序的方法之一。若 Python 只找到 .pyc 字节码文件,而未找到对应的原始 .py 源代码文件,它也很“乐意”运行该程序。

即便 Python 无法在机器上写入字节码, 程序也可以正常工作 —— 字节码会在内存中生成, 并在程序结束时直接被丢弃 (这就是在 IDE 中只点击“运行”不会看到 .pyc 文件的原因,若要显式查看还需要在 IDE 中手动点击“编译”)。然而,由于 .pyc 文件具有加速启动的作用,最好确保在大型程序中能够创建它们。

最后,字节码只会针对那些被导入 (import) 的文件而生成, 而不是顶层的执行脚本 (严格来说,这是一种针对“导入”的优化)。此外,文件仅在程序运行 (或编译) 时才会被导入,而在交互式命令行中输入的命令并不会生成字节码。

2.2 Python 虚拟机 (PVM)

一旦源程序编译成字节码 (或从已存在的 .pyc 文件中载入字节码),便会将字节码发送到被称为 Python 虚拟机 (Python Virtual Machine, PVM) 的程序上来执行。不同于大名鼎鼎的 JAVA 虚拟机 (JVM),Python 虚拟机 (PVM) 相对鲜为人知,其中一个原因在于 PVM 是更为知名的 Python 解释器 (Python Interpreter) 的一部分 (如 CPython 使用了基于堆栈的虚拟机)。但不同于诸如 Vmware 那种 系统虚拟机,此处指的是类似 JVM、CLR 的 程序虚拟机

常见的 Python 解释器有:

简单来说,Python 解释器 由一个 编译器 和一个 虚拟机 构成,编译器负责将源代码转换成字节码文件,而虚拟机负责执行字节码。所以,解释型语言其实也有隐式的编译过程,只不过该编译过程并非直接生成目标代码,而是生成中间代码 (字节码),然后再通过虚拟机来逐行解释执行字节码。

更具体地,PVM 并非一个独立程序,也无须安装。本质上,PVM 可以理解为一个 迭代运行字节码指令的“大循环”—— 一个接一个地完成操作。PVM 作为 Python 的运行时引擎,时常表现为 Python 系统的一部分,并且作为实际运行脚本的组件。从程序运行的技术流程上看,PVM 是 Python 解释器运行程序的最后一步。下图展示了 Python 运行时的简化执行模型:

事实上,程序员编写的由源代码构成的 .py 源文件,先被自动编译为由字节码构成的 .pyc 字节码文件,然后被传递给 Python 虚拟机 PVM 中运行。注意,.pyc 文件是字节码在磁盘上的表现形式,而字节码在 PVM 程序里对应的是 PyCodeObject 对象 (import 模块时创建该对象)。PVM 会字节码当前的上下文环境中,从编译得到的 PyCodeObject 对象中逐条执行字节码指令,从而完成程序的执行。关于更深层的源码分析则大可不必深究。

总体上,操作系统中执行程序离不开两个概念:进程和线程。Python 则通过 PyInterpreterState 和 PyTreadState 分别模拟进程和线程这两个概念。其中,每个 PyThreadState 都对应着一个帧栈,PVM 在多个线程上切换。当 PVM 开始执行时,它会先进行一些初始化操作,最后进入 PyEval_EvalFramEx 函数,它的作用是不断读取编译好的字节码并逐条执行,类似 CPU 执行指令的过程。PyEval_EvalFramEx 函数内部主要是一个 switch 结构,根据字节码的不同执行不同的代码。

总之,程序员只需简单地编写代码并运行文件,而 Python 会负责所有运行这些文件的逻辑。

2.3 性能意义

倘若熟悉 C 和 C++ 这类完全编译语言,则很容易发现 Python 运行模式中的一些差异。例如,Python 的执行流程中通常 没有 build / make 的步骤,而是写完代码立即执行 (run);Python 字节码并非机器的二进制代码 (如 Intel 或 ARM 芯片的指令)。其实,字节码是特定于 Python 的一种表现形式。这就是为什么 Python 代码难以运行得像 C 或 C++ 代码一样快,因为 PVM 循环 (而非 CPU 芯片) 仍需逐行解释字节码,并且相比于 CPU 指令,执行字节码指令需要更多的工作。另一方面,与其他经典的解释器不同,Python 仍有内部的编译步骤,即无需反复地重新分析、分解每行源代码语句的文本 (所以 Python 又比经典的解释型语言更快些)。在上述机制的综合作用下,Python 代码的运行速度介于传统的编译语言和传统的解释语言之间。

2.4 开发意义

Python 执行模型所导致的另一个结果是:Python 的开发和执行环境实际上并无区别。换言之,编译和执行源代码的系统是同一个系统。在 Python 中,编译器总是在运行时出现,并作为运行程序系统的一部分,从而将大大缩短开发周期,提升开发效率。在程序开始执行前,无需预编译和链接,只需简单地输入并运行代码即可。这同样让 Python 带上了更浓厚的 动态语言 色彩:在运行时, Python 程序去构建并执行另一个 Python 程序是可能的,且往往非常方便。例如,eval 和 exec 内置模块能够接受并运行包含Python 程序代码的字符串。上述结构是 Python 能够实现产品定制的原因:因为 Python 代码可以动态地被修改,用户可以改进系统内部的 Python 部分,而无需拥有或编译整个系统的代码。

总而言之,Python 完全不需要初始的编译阶段,所有的事情都是在程序运行时发生的,甚至包括建立函数和类的操作以及模块的链接。而这些工作对于静态语言而言,往往发生在执行之前。

三、小结


 参考文献:

《Learning Python 5th》

https://blog.csdn.net/cqcre/article/details/91456902

https://zhuanlan.zhihu.com/p/38855233

https://www.cnblogs.com/webber1992/p/6597166.html

https://www.cnblogs.com/Bottle-cap/articles/10123700.html

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读