赞
踩
当我们通过命令行传入参数的方式调用 python 解释器去运行一个模块的时候,比如: $ python test.py 图2.1中所示的过程将开始进行。(当然这只是其中一种运行 Python 程序的方式比如也可以在交互模式下单行运行,对于交互模式,这里暂时不做讨论。) 基于Python3.7
Python 可执行程序是一个用 C 语言编写的程序。当它被执行的时候,所发生的事情其实就和其他 C 语言程序(比如 Linux 内核或是一个简单的 hello world 程序)差不多。请花一点时间来理解一下,Python 可执行程序只是一个用来运行程序的程序,可以结合 C 与 汇编或者 LLVM 的关系来理解。当我们使用解释器运行一个模块的时候,最开始会运行一个与平台相关的标准初始化过程。
所有讨论都假设在类 Unix 操作系统中,Windows 下会有些许不同。
C 运行时环境会包办了所有的初始化操作,像是加载库、检查与设置环境变量等等。然后 python 可执行程序的 main 方法开始运行就像任何其他普通的 C 程序那样。
python 的 main 函数位于 Programs/python.c文件中。 main 函数会调用位于 Modules/main.c中的 Py_Main 函数,它处理解释器的初始化过程,包括:解析命令行参数、设置程序的 flags、读取环境变量、运行 hook、哈希初始化等等。作为初始化过程的一部分, Python/pylifecycle.c中的 Py_Initialize 函数会被调用,它会初始化两个比较重要的数据结构:解释器状态与线程状态。
看一眼解释器状态与线程状态的定义,它能给我们一些关于它们功能的信息。这两个数据结构由一些指向特定字段的指针组成,它们带有程序执行所需要的一些信息。首先我们来看解释器状态在源码中的定义(Include/pystate.h),以下是其中比较重要的部分:
//解释器状态数据结构定义如下 typedef struct _is { struct _is *next; struct _ts *tstate_head; PyObject *modules; PyObject *modules_by_index; PyObject *sysdict; PyObject *builtins; PyObject *importlib; PyObject *codec_search_path; PyObject *codec_search_cache; PyObject *codec_error_registry; int codecs_initialized; int fscodec_initialized; PyObject *builtins_copy; } PyInterpreterState;
Python 程序员应该或多或少会对这些字段名字中的词有一些认识比如:sysdict,builtins,codec 等。
程序的运行必须在一个线程之中进行,thread state structure 包含了一个线程执行 python code object 所需的信息,关于 thread state 定义的其中一部分代码如下(Include/pystate.h):
//线程状态数据结构定义如下 typedef struct _ts { struct _ts *prev; struct _ts *next; PyInterpreterState *interp; struct _frame *frame; int recursion_depth; char overflowed; /* The stack has overflowed. Allow 50 more calls to handle the runtime error. */ char recursion_critical; /* The current calls must not cause a stack overflow. */ int stackcheck_counter; /* 'tracing' keeps track of the execution depth when tracing/profiling. This is to prevent the actual trace/profile code from being recorded in the trace/profile. */ int tracing; int use_tracing; Py_tracefunc c_profilefunc; Py_tracefunc c_tracefunc; PyObject *c_profileobj; PyObject *c_traceobj; /* The exception currently being raised */ PyObject *curexc_type; PyObject *curexc_value; PyObject *curexc_traceback; PyObject *dict; /* Stores per-thread state */ int gilstate_counter; } PyThreadState;
关于这两个数据结构,后面的章节中还会有进一步讨论。初始化过程还会设置一些重要的机制,比如基本的 stdio等。
一旦完成了所有的初始化,Py_Main 函数会调用 pymain_run_file函数,随后发生调用:
PyRun_AnyFileExFlags ->
PyRun_SimpleFileExFlags ->
PyRun_FileExFlags ->
PyParser_ASTFromFileObject
在 PyRun_SimpleFileExFlags 函数调用中,main namespace 将被创建,文件内容将在其中被运行。它也将检查文件的 pyc 版本是否存在(pyc 文件是一个储存了已经被执行过的py文件编译后的代码(字节码)的文件)。当文件的 pyc 版本存在的时候,将会尝试以二进制形式读取并执行它。而当 pyc 文件不存在的时候,会顺着 PyRun_FileExFlags 向下调用, PyParser_ASTFromFileObject函数会接着调用 PyParser_ParseFileObject函数,它会读取被执行模块的内容并建立 parse tree,然后将 parse tree 传递给 PyAST_FromNodeObject根据 parse tree 创建 AST。(如果你阅读这些代码,你会看到很多的 Py_INCREF 和 Py_DECREF。这些内存管理函数会在后面详细讨论。CPython 通过引用计数管理对象的生命周期。当一个新的对象引用产生的时候,计数通过Py_INCREF增加,当一个引用离开作用域的时候会通过Py_DECREF减少。)
AST 生成后会被传给 run_mod函数,这个函数会接着调用 PyAST_CompileObject 根据 AST 生成 code object。并且在调用 PyAST_CompileObject 生成字节码的时候会经过一个简单的 peephole optimizer 进行字节码优化。随后 run_mod 会调用 PyEval_EvalCode 随之产生函数调用:
PyEval_EvalCode ->
PyEval_EvalCodeEx ->
_PyEval_EvalCodeWithName ->
PyEval_EvalFrameEx
当所有的指令都被执行以后,Py_Main 将继续执行一些清理(clean up)过程。就像 Py_Initialize 所做的那样,Py_Finalize 会被调用完成一些清理任务,比如等待线程退出、调用 exit hooks、清理解释器分配的仍然被使用的内存等等。
上面我们在一个较高的层面上对 python 解释器如何执行程序进行了描述,但还有大量的细节没有被讨论,接下来的文章中我们将深入进去提供更多的细节信息。
参考视频
参考课程
参考文章
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。