在Python虚拟机内部。第1部分



大家好。我最终决定弄清楚Python解释器如何工作。为此,他开始研究一本文章,并同时构思将其翻译成俄文。事实是,翻译不允许您遗漏难以理解的句子,并且材料同化的质量会提高。对于可能出现的错误,我深表歉意。我总是尝试尽可能正确地翻译,但这是主要问题之一:根本没有在俄语对等词中提到某些术语。

翻译说明
Python , «code object», ( ) . , .

— Python, - , : , , ( ! ) , , - ( ) .. — () - str (, Python3, bytes).

介绍


Python编程语言已经存在了一段时间。第一版的开发是由Guido Van Rossum于1989年开始的,自那时以来,该语言已经发展成为最受欢迎的语言之一。Python用于各种应用程序:从图形界面到数据分析应用程序。

本文的目的是在解释器的幕后进行,并提供有关如何执行以Python编写的程序的概念性概述。本文将考虑使用CPython,因为在撰写本文时,它是最流行和最基本的Python实现。

在本文中,Python和CPython被用作同义词,但是任何提及Python的意思是CPython(用C实现的python版本)。其他实现包括PyPy(在Python的有限子集中实现的Python),Jython(在Java虚拟机上的实现)等。

我喜欢根据解释器的调用方式,将Python程序的执行分为两个或三个主要步骤(下面列出)。本文将在不同程度上介绍这些步骤:

  1. 初始化 -此步骤涉及设置python进程所需的各种数据结构。当程序通过解释器的外壳以非交互模式执行时,很可能会发生这种情况。
  2. — , : , , .
  3. — .

生成“解析”树以及抽象语法树(ASD)的机制与语言无关。因此,由于Python中使用的方法与其他编程语言的方法类似,因此我们不会涉及太多主题。另一方面,在Python中从ADS构造符号表和代码对象的过程更加具体,因此,它值得特别注意。它还讨论了编译代码对象和所有其他数据结构的解释。我们涵盖的主题将包括但不限于:构建符号表和创建代码对象,Python对象,框架对象,代码对象,功能对象,操作码,解释器循环,生成器和用户类的过程。

本材料适用于对学习CPython虚拟机的工作方式感兴趣的任何人。假定用户已经熟悉python并且了解该语言的基础知识。在研究虚拟机的结构时,我们会遇到大量的C代码,因此,对C语言有基本了解的用户更容易理解材料。因此,基本上,您需要熟悉此材料:了解更多有关CPython虚拟机的愿望。

本文是对口译员内部工作进行研究时个人笔记的扩展版本。PyCon视频,学校讲座和此博客中有很多高质量的东西如果没有这些奇妙的知识来源,我的工作就不会完成。

在本书的最后,读者将能够理解Python解释器如何执行程序的复杂性。这包括程序执行的各个阶段以及对程序至关重要的数据结构。首先,我们将鸟瞰一下执行普通程序,将模块名称通过命令行传递给解释器时发生的情况。可以按照《Python开发人员指南》从源代码安装CPython可执行代码

本书使用Python 3版本。

30,000英尺


本章讨论解释器如何执行Python程序。在接下来的章节中,我们将研究此“难题”的各个部分,并对每个部分进行更详细的描述。无论用Python编写的程序的复杂性如何,此过程始终是相同的。 Yaniv Aknin在其有关Python Internal的系列文章中给出的出色解释为我们的讨论奠定了主题。

可以从命令行执行源模块test.py(将其作为$ python test.py形式的参数传递给Python解释程序时)。这只是调用Python可执行文件的一种方式。我们还可以启动交互式解释器,将文件的行作为代码执行,等等。但是这种方法和其他方法使我们不感兴趣。正是将模块作为参数(在命令行中)传输到可执行文件(图2.1),才能最好地反映代码实际执行中涉及的各种操作的流程。


图2.1:运行时的流。

python可执行文件是常规C程序,因此,在调用该可执行文件时,会发生类似于linux内核或简单hello world程序中存在的进程。花一点时间来理解:python可执行文件只是另一个启动您自己的程序。这样的“关系”存在于C语言和汇编程序(或llvm)之间。当使用模块名称作为参数调用python可执行文件时,标准的初始化过程(取决于执行的平台)开始。

本文假定您使用的是基于Unix的操作系统,因此某些功能在Windows上可能会有所不同。

启动时C语言执行其所有“魔术”初始化-加载库,检查/设置环境变量,然后,像其他所有C程序一样,启动python可执行文件的主要方法。 Python的主要可执行文件位于./Programs/python.c中,并执行一些初始化(例如,复制传递给模块的程序命令行参数的副本)。然后,main函数调用位于./Modules/main.c中Py_Main函数。它处理解释器的初始化过程:分析命令行参数,设置标志,读取环境变量,执行钩子,随机化哈希函数等。也叫pylifecycle.c中Py_Initialize是两个非常重要的数据结构,它处理解释程序和流状态数据结构的初始化。

检查解释器数据结构和流状态的声明可以清楚地说明为什么需要它们。解释器的状态和流只是带有指向字段的指针的结构,这些字段包含执行程序所需的信息。解释器状态数据是通过typedef创建的(只是将C中的该关键字视为类型定义,尽管并非完全如此)。清单2.1中显示了此结构的代码。

 1     typedef struct _is {
 2 
 3         struct _is *next;
 4         struct _ts *tstate_head;
 5 
 6         PyObject *modules;
 7         PyObject *modules_by_index;
 8         PyObject *sysdict;
 9         PyObject *builtins;
10         PyObject *importlib;
11 
12         PyObject *codec_search_path;
13         PyObject *codec_search_cache;
14         PyObject *codec_error_registry;
15         int codecs_initialized;
16         int fscodec_initialized;
17 
18         PyObject *builtins_copy;
19     } PyInterpreterState;

代码2.1:解释器状态数据结构

长期使用Python编程语言的任何人都可以识别此结构中提到的几个字段(sysdict,builtins和codec)。

  1. * next字段是对解释器另一个实例的引用,因为在同一进程中可以存在多个Python解释器。
  2. * tstate_head字段指示执行的主线程(如果程序是多线程的,则解释器是用于通过程序创建的所有线程共用)。我们将在短期内对此进行详细讨论。
  3. 模块,modules_by_index,sysdict,buildinsimportlib可以说明一切。它们全部都定义为PyObject的实例,该实例是Python虚拟机中所有对象的根类型。在接下来的章节中将更详细地讨论Python对象。
  4. 编解码器*相关的字段包含有助于下载编码的信息。这对于解码字节非常重要。

程序执行必须在线程中发生。流的状态结构包含该流执行某些代码对象所需的所有信息。清单2.2显示了部分流数据结构。

 1     typedef struct _ts {
 2         struct _ts *prev;
 3         struct _ts *next;
 4         PyInterpreterState *interp;
 5 
 6         struct _frame *frame;
 7         int recursion_depth;
 8         char overflowed; 
 9                         
10         char recursion_critical; 
11         int tracing;
12         int use_tracing;
13 
14         Py_tracefunc c_profilefunc;
15         Py_tracefunc c_tracefunc;
16         PyObject *c_profileobj;
17         PyObject *c_traceobj;
18 
19         PyObject *curexc_type;
20         PyObject *curexc_value;
21         PyObject *curexc_traceback;
22 
23         PyObject *exc_type;
24         PyObject *exc_value;
25         PyObject *exc_traceback;
26 
27         PyObject *dict;  /* Stores per-thread state */
28         int gilstate_counter;
29 
30         ... 
31     } PyThreadState;

清单2.2:流状态数据

结构的一部分在以下各章中将详细讨论解释器数据结构和流状态。初始化过程还设置了导入机制以及基本stdio。

完成所有初始化后,Py_Main调用run_file函数(也位于main.c模块中)。以下是一系列函数调用:PyRun_AnyFileExFlags-> PyRun_SimpleFileExFlags-> PyRun_FileExFlags-> PyParser_ASTFromFileObject。PyRun_SimpleFileExFlags创建将在其中执行文件内容的__main__命名空间。它还会检查文件的pyc版本是否存在(pyc文件是包含源代码已编译版本的简单文件)。如果存在pyc版本,将尝试将其读取为二进制文件,然后运行它。如果缺少pyc文件,则调用PyRun_FileExFlags,依此类推。 PyParser_ASTFromFileObject函数调用PyParser_ParseFileObject,后者读取模块的内容并从该模块构建解析树。然后,将创建的树传递给PyParser_ASTFromNodeObject,后者从中创建一个抽象语法树。

, Py_INCREF Py_DECREF. , . CPython : , , Py_INCREF. , , Py_DECREF.

调用run_mod时生成AST 。此函数调用PyAST_CompileObject该函数从AST创建代码对象。请注意,在PyAST_CompileObject调用期间生成的字节码是通过简单的窥孔优化器传递的,该优化器在创建代码对象之前对生成的字节码进行了低级优化。然后,run_mod函数ceval.c文件中的PyEval_EvalCode函数应用于代码对象。这将导致另一系列的函数调用:PyEval_EvalCode-> PyEval_EvalCode-> _PyEval_EvalCodeWithName-> _PyEval_EvalFrameEx。代码对象以一种或另一种形式作为参数传递给大多数这些函数。_PyEval_EvalFrameEx-这是一个正常的解释器循环,用于处理代码对象的执行。但是,不仅使用代码对象作为参数来调用它,还使用框架对象来调用它,该对象具有作为属性的引用代码对象的字段。该框架提供了执行代码对象的上下文。简单来说:解释器循环从指令数组中连续读取指令计数器指示的下一条指令。然后,它执行以下指令:在过程中从值堆栈中添加或删除对象,直到将其清空到要执行的指令数组中(好吧,否则会破坏循环)。

Python提供了一组可用于检查实际代码对象的函数。例如,一个简单的程序可以编译为一个代码对象,然后反汇编以获得由python虚拟机执行的操作码。如清单2.3所示。

1         >>> def square(x):
2         ...     return x*x
3         ... 
4 
5         >>> dis(square)
6         2           0 LOAD_FAST                0 (x)
7                     2 LOAD_FAST                0 (x)
8                     4 BINARY_MULTIPLY     
9                     6 RETURN_VALUE        

代码清单2.3:在Python中反汇编函数

头文件./Include/opcodes.h包含Python虚拟机的所有指令/操作码的完整列表。操作码非常简单。以清单2.3中的示例为例,它包含一组四个指令。LOAD_FAST将其参数的值(在本例中为x)加载到值堆栈上。 python虚拟机基于堆栈,因此操作码操作的值从堆栈中``弹出'',并将计算结果推回堆栈中以供其他操作码进一步使用。然后BINARY_MULTIPLY从堆栈中弹出两个项目,对两个值执行二进制乘法,然后将结果压回堆栈。返回值说明从堆栈中检索一个值,将该对象的返回值设置为此值,然后退出解释器循环。如果看清单2.3,很明显这是一个非常简单的简化。

当前对解释器循环的解释未考虑许多细节,这些细节将在后续章节中进行讨论。例如,以下是我们未收到答案的问题:

  • LOAD_FAST语句加载的值从何而来?
  • 自变量来自哪里,这些自变量用作指令的一部分?
  • 嵌套函数和方法调用如何处理?
  • 解释器循环如何处理异常?

完成所有指令后,Py_Main函数将继续执行,但这一次将启动清理过程。如果在解释器启动期间调用Py_Initialize进行初始化,则将调用Py_FinalizeEx进行清理。此过程包括等待线程退出,调用所有退出处理程序以及释放由解释器分配的仍在使用的内存。

因此,我们看了运行脚本时Python可执行文件中发生的进程的“高级”描述。如前所述,仍有许多问题需要解答。将来,我们将深入研究口译员,并详细考虑每个阶段。我们将在下一章开始描述编译过程。

All Articles