Python 解释器与字节码

解释器与字节码

引言

当我们编写好 Python 代码后,便把它交给了 Python 解释器去执行,解释器就如同代码背后的管道一般,对代码进行搬运和处理,直至输出预期内的结果。那么代码在解释器内部的运行原理是怎样的?这一节我们便从 Python 解释器、字节码、Python 虚拟机、Python的不同实现等几个方面出发, 对 Python 的内部运行机制进行一定的剖析。

Python 是解释型语言吗?

对于计算机而言,它只能理解机器语言,但对于我们通常所使用的编程语言而言,一般都属于高级语言,比如 Python、Java、C++,这些高级语言都要转换成机器语言才能被计算机执行。笼统的说,高级语言被转换为机器语言的方式可分为编译和解释,所以对于高级语言来说,从执行的角度可被分为编译型和解释型。

  • 编译型语言所编写的程序在运行前会被编译为机器码,运行时直接使用编译后产出的机器码即可。
  • 解释型语言在运行前不进行编译,但需要在运行时通过解释器将源代码解释为机器码。这种在运行时的解释过程以及可能存在的相同代码的重复解释,也是导致解释型语言在执行效率上相对较低的重要原因。但相反的是,通常解释型语言的跨平台能力相对较好,只需要在不同平台上安装相应的解释器即可。

对于 Python 而言,它虽然没有显式的编译过程,但它和传统的解释型语言仍具有一些差别。在 Python 内部仍存在编译过程,在运行 Python 程序时,源代码首先会被自动编译为字节码而并非直接被解释执行,但这种字节码并不是机器对应的二进制码,在运行时,字节码会在 Python 虚拟机内被进一步解释执行。

作为一种解释型语言来讲,Python 的执行效率是相对较高的,但相对于编译型语言,它的执行效率却是比较低的。如果语言的执行效率对你所要完成的任务很关键,那么在 Python 中的第一个解决方案是选择通过以 C 或 C++ 编写第三方库来对你的 Python 代码进行扩展,因为 Python 的一个优势便是可以很好的和 C/C++ 进行结合,将核心的计算任务放在这些语言中完成,以提高程序的执行效率。另一个解决方案是使用支持即时编译 ( JIT,just-in-time ) 的解释器来替换 CPython,例如 PyPy,它优化了代码生成和 Python 程序的执行速度,这一点我们在下面 Python 实现中也会提到。

最后,在这一点中不得不说的是,一些没有深入了解和使用 Python 的用户仅仅将 Python 作为一种简单的脚本语言,并带有一些 ”偏见“ 的认为 Python 不适用于构建大型项目,但事实(尤其是 Python 在国内外众多顶尖的软件团队中的大量应用)已经充分的证明 Python 的能力其实远超过我们的想象。随着Typing hint 的到来,Python 也渐渐地开始能够构建一些大型的应用,性能也还可以。

Python 解释器是什么?

我们提到了 Python 代码会被编译为字节码再进行其他处理,这些编译和处理的过程都是在 Python 解释器中进行的。Python 解释器是运行 Python 代码的程序,无论在 Linux 或 Windows 平台,我们想要运行 Python 代码,就必须先安装 Python 解释器。如下图所示,Python 解释器是在 Python 代码和计算机硬件之间的处理层。

在这里插入图片描述

在一些 Python 脚本中,我们经常在首行看到 #!/usr/bin/env python,这便是用来指示执行该文件时所使用的解释器。在 Unix 中,一个被解释执行的文件可以使用 #! 在第一行指示要使用的解释器。/usr/bin/env 将会在 $PATH 环境变量中查找要使用的解释器,这种写法相对灵活,与之对应的是使用 #!/usr/bin/python 这样的硬编码写法,其灵活性便相对较差一些。

注意,以上的两种写法都是针对 $./***.py 这种普通程序或者 bash 脚本的执行方式,如果使用 $python ***.py 执行,系统将根据我们的输入调用对应解释器,不需要在脚本中指定解释器。

Python 虚拟机

这里我们对代码在 Python 解释器中的处理过程进一步细化。

  • 首先解释器读取 Python 代码,进行必要的语法检查,确保对应的指令格式正确无误。
  • 接着,Python 代码会被编译成 Python 字节码,这种语言类似于汇编语言,但包含高级指令。
  • 最后,这些字节码会被发送到 Python 虚拟机( PVM,Python Virtual Machine )中进行处理,这是 Python 解释器中的最后的一个步骤。在这里,虚拟机将会遍历代码对象对应的 Python 字节码指令并解释执行这些指令。虚拟机的核心则是处理操作码的计算循环,也称求值循环( evaluation loop )。该循环在实现上并没有太多特别之处,主要包括一个 for 循环和一些用于匹配大量操作码的 switch 语句。

在这里插入图片描述

实际上,在这个过程中 Python 解释器远远要比上述的流程复杂,其中可能会包含着特定于某些平台的优化、虚拟机优化(比如操作码的预测)、字节码执行前的初始化等等。需要注意的是,我们这里的描述有可能会随着 Python 版本更迭产生变化。另外,这里我们对于这些处理过程的介绍均基于 Python 的官方实现 CPython 解释器(官方 Python 解释器是使用 C 进行编写的,称为 CPython 解释器,关于 Python 的实现,我们在下面有对应的讲解)。

字节码

字节码文件与后缀扩展

Python 源代码在编译后生成的字节码是一种不依赖于平台的中间格式(“中间语言”),是 Python 代码在 CPython 解释器内部的表示形式。执行编译后的字节码不仅省去了将源码编译为字节码的过程,而且相比于直接使用 Python 源代码执行来说,字节码在执行效率上有较大的提升。我们在上面已经提到,在虚拟机中会执行字节码对应的机器码,但需要注意的是,字节码不能完全保证在不同的 Python 虚拟机上使用,并且也不能完全保证在不同的 Python 版本之间兼容。

字节码被存储在 .pyc 为后缀的文件中。执行字节码相比于执行源代码而言,提升了执行速度,而存储字节码则可以提升启动速度,避免了在每次启动运行时都要进行将源代码编译为字节码的过程。如果使用相同的 Python 版本,并且源代码并没有发生改变的情况下,Python 将会使用已保存的字节码文件,从而在启动时省去编译的过程。

作为扩展,我们在这里简单的了解 Python 生态中的一些文件后缀所表示的含义:.py 表示源代码文件;.pyc 表示编译后的字节码文件;pyo 是 Python 3.5 之前使用的一种文件格式,通过 -O-OO 标志调用解释器,对生成字节码进行优化(目前主要是删除 assert 语句等)并存储在 pyo 文件中,在 Python 3.5 之后,去除了 pyo 文件格式,使用 pyc 文件来同时表示未优化和优化的字节码(更多细节可以参考 PEP 488 );pyi 表示 “存根” 文件(Stub file),包含对模块公共接口的描述, 不包含具体的实现(更多细节可以参考 PEP 484 );pyd 表示作为 Windows DLL 生成的 Python 文件(更多细节可参考 *.pyd 文件和DLL文件相同吗?);pyx 表示由 Cython(一种为 Python 编写 C 扩展的语言)编写的源代码文件(更多细节可参考 Cython programming language )。

手动生成字节码文件

在 Python 的标准库中,有两个模块可以帮助我们生成字节码文件,分别为 py_compilecompileall ,前者主要提供了从单个源文件中编译生成字节码文件的相关函数,后者主要提供了从目录中编译源文件的相关函数,这两个模块可以用于包或模块安装时创建字节码文件,特别是在某些用户可能无权在包含源代码的目录中写入字节码文件的情况下。

下面的代码 PyCharm 的 Python console 中执行,我们重新将 PyCharm 的 Python 版本切换到 Python 3.7,并将之前生成的包括 __pycache__ 目录在内的所有字节码文件删除。

>>> py_compile.compile('./003_concatenator.py')
'./__pycache__/003_concatenator.cpython-37.pyc'

使用 dis 模块分析字节码

我们可以通过如下的方式去查看所示函数对象对应的字节码,函数的 __code__ 属性表示编译后的函数体的代码对象 ,co_code 表示原始编译字节码 (更多细节可参考数据模型中代码对象部分)。

>>> def function_one():
...     print("hello world")
...     
>>> function_one.__code__.co_code
b't\x00d\x01\x83\x01\x01\x00d\x00S\x00'

对于这些字节码来说,我们无法理解它们,这时可以借助 Python 标准库中的 dis 模块( Disassembler for Python bytecode )来进行字节码反汇编,它的主要作用是将 CPython 字节码转换为一种人类可读的形式来进行字节码分析。需要注意的是,字节码作为 CPython 解释器的实现细节可能会在不同的 Python 版本之间发生变动。我们可以使用 dis.dis(x) 来反汇编 x 对象(也可以使用 dis.disco(x.__code__)),其中 x 可以表示模块、类、方法、函数、生成器、异步生成器、协程等。

具体的看下面的示例的输出结果中,第一列中的 2 表示源代码中的行号,第二列的数字表示指令的地址( Python 3.6 之后每条指令使用 2 个字节,在这之前的字节数会根据指令的不同会发生变化),第三列则表示人类可读的操作码名称 opname (与之对应的 opcode 则表示操作的数字代码),比如 LOAD_GLOBAL 表示将 名称为 co_names[namei] 的全局对象推入栈顶,其中 co_names 表示包含字节码所使用的局部变量名称的元组,namei 则是 name 在代码对象的 co_names 属性中的索引。对于这部分感兴趣的同学可以结合 dis数据模型中代码对象部分inspect 等模块一起来更详细的理解相关逻辑的具体含义。剩下的第四列和第五列则分别表示操作参数和参数解释 。

dis模块 的每段的含义如下

源码行号 | 指令在函数中的偏移 | 指令符号 | 指令参数 | 实际参数值

>>> import dis
>>> dis.dis(function_one)
  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('hello world')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
>>> dis.opname
['<0>', 'POP_TOP', 'ROT_TWO', ..., '<254>', '<255>']
>>> dis.opmap
{'POP_TOP': 1, 'ROT_TWO': 2, 'ROT_THREE': 3, ..., 'CALL_FINALLY': 162, 'POP_FINALLY': 163}
>>> dis.opmap['LOAD_GLOBAL']
116

也可以以命令行方式使用 dis 模块。除此之外,还可以使用 dis 模块中的其他的分析函数,在这里便不再赘述。

func.py

# -*- coding: utf-8 -*-
"""
@Time    : 2022/7/30 16:23
@Author  : Frank
@File    : func.py
"""


def function_one():
    print("hello world")
    

使用 dis 模块 来查看 字节码

$ python -m dis func.py

(venv) ➜ python -m dis func.py
  6           0 LOAD_CONST               0 ('\n@Time    : 2022/7/30 16:23\n@Author  : Frank\n@File    : func.py\n')
              2 STORE_NAME               0 (__doc__)

  9           4 LOAD_CONST               1 (<code object function_one at 0x7f94e73146f0, file "func.py", line 9>)
              6 LOAD_CONST               2 ('function_one')
              8 MAKE_FUNCTION            0
             10 STORE_NAME               1 (function_one)
             12 LOAD_CONST               3 (None)
             14 RETURN_VALUE

Disassembly of <code object function_one at 0x7f94e73146f0, file "func.py", line 9>:
 10           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('hello world')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE


Python 的不同实现

Python 的实现是指使用何种方式、何种语言来实现 Python,在不同的实现下,同样的代码所实现的功能通常是相同的,我们在此前的讲解都是基于 Python 的官方标准实现 CPython 来完成的。目前 Python 的实现方式除了 CPython 外,还有 PyPy、Jython、IronPython、Stackless Python 等。

CPython

CPython 使用 C 语言编写而成,是 Python 语言的参考实现。在语言特性的完整性与及时性、运行稳定性多个方面,它是最优的选择。同时,在库支持、C 和 C ++ 扩展等方面,CPython 目前也具有非常明显的优势。但 CPython 目前仍存在两个较为显著的问题,一方面是全局解释器锁 GIL 对多线程执行的限制;另一方面,CPython 未支持即时编译 JIT(一种运行时将部分字节码整体编译为机器码的机制),使得在代码的执行效率上相对较低。

PyPy

PyPy 作为 Python 的另一个最重要的实现,采用 Python 本身实现。PyPy 吸纳了 Psyco 即时编译器的基础,它最重要的特点便是支持即时编译 JIT ,在运行时将部分字节码转换为机器码,使得在大部分情况下 Python 程序的运行速度得到了显著提升。PyPy 在内存占用上相比于 CPython 也具有一些优势,同时吸收了 Stackless Python 的设计思想,并且未使用引用计数进行垃圾回收。

但与此同时,在利用 C 语言编写而成的 Python 扩展以及相关新特性的支持上,PyPy 还有很多欠缺的地方,但它作为 Python 的一个重要方向,仍值得 Python 开发者持续关注。另外,截止目前 PyPy 并未去除 GIL 。

Jython 和 IronPython

Jython 是 Python 的 Java 实现,可以在 Python 代码中使用 Java 类,其将代码编译为 Java 字节码。Jython 的主要目的是在 Python 代码中无缝的使用 Java 类,从而可以在 Java 系统中使用 Python 来脚本化 Java 代码。

IronPython 最初由微软开发,和 Jython 的设计思想类似,其将 Python 与 Windows 上的 .Net 框架相继承。值得一提的是,在 Jython、IronPython 中均没有全局解释器锁 GIL,因此多线程可以有效的利用多核,它们的主要缺点是目前无法利用众多 C 语言编写而成的 Python 扩展,并且和 CPython 相比,目前在效率和稳定性上并无优势。

Stackless

Stackless Python 是在 CPython 基础上针对并发而进行增强的 Python 实现。见名思义,Stackless Python 不依赖 C 语言调用栈进行状态保存。其最显著的特点是使用轻量级的微线程代替依赖内核进行上下文切换与调度的原生线程。由 Stackless Python 衍生的 greenlet 在众多框架中得到了广泛的应用。同时,Stackless Python 的思想也促进了协程生态的产生和发展。

总结

Python 这门语言是一种解释性语言,但是也需要一些中间结果(字节码),python的解释器才能翻译。 字节码 会被送到 PVM (Python Virtual Machine)中进行处理的。 当然也不是每次都需要编译生成 字节码文件 ,只有在Python代码改变的时候才会重新进行编译,生成一个 __pycache__ 的目录, 里面存放的就是 字节码文件啦. 现在最主流的Python 实现 是Cpython 实现的,也就是使用C语言实现的Python,其他的实现并没有形成一个非常强大的生态,可以作为了解即可。

参考文档

解释器与字节码

Understanding Python Bytecode

Python 字节码介绍

死磕python字节码手工还原python源码,牛皮!

分享快乐,留住感动. '2022-07-31 16:51:55' --frank
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值