在 Python 编程的天下里,我们每天都在使用 python 命令运行步调,但你是否曾好奇这背后究竟发生了什么?
本文将初步探究 CPython(Python 中最流行的实现)的一些内部机制,为了更好的来理解 Python 语言的底层运作。
1. CPython 简介
CPython 是用** C 语言**编写的 Python 解释器,在浩繁 Python 实现(如 PyPy、Jython、IronPython 等)中,它以其原创性、精良的维护性和高人气脱颖而出。
了解 CPython 的一些内部机制,对我们学习和使用Python语言本身也有很大的帮助:
- 有助于深入理解 Python 语言:了解实现细节能让我们更轻松地掌握 Python 的一些特性
- 实现细节在实际应用中至关重要:对象存储方式、垃圾回收机制以及多线程协调等方面的知识,对于理解语言的适用性、局限性、性能评估和效率检测都非常关键
- CPython 提供的 Python/C API 允许我们用 C 扩展 Python 或在 C 中嵌入 Python,而有用使用该 API 需要对 CPython 的工作原理有深入理解
CPython是开源的,源码在github.com上:https://github.com/python/cpython
每个版本的Python都有相应的CPython实现,我现在使用的Python3.12,
所以本文后续如果有参考的代码,参考的是CPython 3.12分支中的代码。
2. Python执行的流程
宏观上来来看,一个Python步调的执行大致分为三个阶段:
第一个阶段是初始化阶段,CPython 在此阶段初始化运行 Python 所需的数据结构,包括内置类型、配置和加载内置模块、设置导入系统等。
这个阶段虽然重要,但常常被忽视,因为它重要为步调的运行做一些准备工作。
第二个阶段是编译阶段,CPython是解释器,虽不生成呆板码,但会将源代码转换为中间表示形式。
它会剖析源代码构建抽象语法树(AST),从 AST 生成字节码,并进行一些字节码优化。
这个阶段虽然名称是编译,但是和C/C++这类编译型语言的编译不是一个含义。
Python代码经过编译之后的字节码是可以检察的,好比下面简单写一个加法函数。- def add(x, y):
- return x + y
复制代码 命令行中使用:python.exe -m dis .\cpython-vm.py检察字节码。
LOAD_FAST 指令将局部变量压入栈中,
BINARY_ADD 指令从栈中弹出两个对象,将它们相加,并将结果压回栈中。
最后,RETURN_VALUE 指令弹出栈顶的任何内容,并将结果返回给调用者。
最后一个阶段是解释阶段,CPython 的核心是一个执行字节码的假造机。字节码是一系列指令,每条指令由一个操作码和一个参数组成。
CPython 假造机是基于栈的,通过栈来存储和检索数据,执行指令。字节码执行在一个巨大的求值循环中进行,直到没有指令可执行或发生错误。
3. 核心概念
通过CPython假造机的内部机制来了解Python的底层原理,起首要关注的就是CPython假造机中的一些核心概念。包括:代码对象,函数对象,帧对象。
3.1. 代码对象
代码对象是 CPython 中存储代码块相关信息的结构,像模块、函数体这类作为独立执行单位的代码,其信息都保存在代码对象里。
它包罗字节码(步调编译后的中间表示形式),以及代码块内使用的变量名列表等关键信息。
从本质上讲,代码对象是对一段可执行代码的抽象表示,为函数的调用、模块的运行提供了必要的指令和数据形貌。
其相关界说在源码文件:cpython/Include/cpython/code.h- #define _PyCode_DEF(SIZE) { \
- PyObject_VAR_HEAD \
- // 省略... \
-
- /* The hottest fields (in the eval loop) are grouped here at the top. */ \
- PyObject *co_consts; /* list (constants used) */ \
- PyObject *co_names; /* list of strings (names used) */ \
- PyObject *co_exceptiontable; /* Byte string encoding exception handling \
- table */ \
- int co_flags; /* CO_..., see below */ \
- \
- /* The rest are not so impactful on performance. */ \
- int co_argcount; /* #arguments, except *args */ \
- int co_posonlyargcount; /* #positional only arguments */ \
- int co_kwonlyargcount; /* #keyword only arguments */ \
- int co_stacksize; /* #entries needed for evaluation stack */ \
- int co_firstlineno; /* first source line number */ \
- \
- /* redundant values (derived from co_localsplusnames and \
- co_localspluskinds) */ \
- int co_nlocalsplus; /* number of spaces for holding local, cell, \
- and free variables */ \
- int co_framesize; /* Size of frame in words */ \
- int co_nlocals; /* number of local variables */ \
- int co_ncellvars; /* total number of cell variables */ \
- int co_nfreevars; /* number of free variables */ \
- uint32_t co_version; /* version number */ \
- \
- PyObject *co_localsplusnames; /* tuple mapping offsets to names */ \
- PyObject *co_localspluskinds; /* Bytes mapping to local kinds (one byte \
- per variable) */ \
- PyObject *co_filename; /* unicode (where it was loaded from) */ \
- PyObject *co_name; /* unicode (name, for reference) */ \
- PyObject *co_qualname; /* unicode (qualname, for reference) */ \
- PyObject *co_linetable; /* bytes object that holds location info */ \
- PyObject *co_weakreflist; /* to support weakrefs to code objects */ \
- // 省略...
- }
- /* Bytecode object */
- struct PyCodeObject _PyCode_DEF(1);
复制代码 界说比力长,这里只列出了一部门。
以上一节中示例中的函数add(x, y)函数为例,CPython 会为add函数体创建一个代码对象。
在这个代码对象中,字节码部门记载了如何加载变量x和y、执行加法操作并返回结果等指令序列。
同时,还包罗co_argcount(参数数量,此处为 2)等属性,这些属性形貌了函数的参数使用情况。
别的,代码对象还会记载函数界说所在的文件名(co_filename )、起始行号(co_firstlineno)等信息,方便调试和代码分析。
3.2. 函数对象
函数对象不仅仅包罗可执行代码(即代码对象),还存储了与函数相关的其他重要信息,如函数名、文档字符串(docstring)、默认参数、外部作用域变量值等。
函数对象将代码对象与函数运行所需的上下文信息整合在一起,使得函数可以在不同的情况中被精确调用和执行。
多个函数对象可以引用同一个代码对象,通过不同的外部信息实现不同的功能,比方闭包的实现。
其相关界说在源码文件:cpython/Include/cpython/funcobject.h- typedef struct {
- PyObject_HEAD
- _Py_COMMON_FIELDS(func_)
- PyObject *func_doc; /* The __doc__ attribute, can be anything */
- PyObject *func_dict; /* The __dict__ attribute, a dict or NULL */
- PyObject *func_weakreflist; /* List of weak references */
- PyObject *func_module; /* The __module__ attribute, can be anything */
- PyObject *func_annotations; /* Annotations, a dict or NULL */
- PyObject *func_annotate; /* Callable to fill the annotations dictionary */
- PyObject *func_typeparams; /* Tuple of active type variables or NULL */
- vectorcallfunc vectorcall;
- uint32_t func_version;
- } PyFunctionObject;
复制代码 3.3. 帧对象
帧对象在 CPython 中用于跟踪代码执行过程中的各种状态信息。
当假造机执行代码对象时,帧对象负责记载变量的值、维护值栈(用于指令执行时的数据存储和操作),还会记载代码执行的位置(如当前行号、上一条执行的指令位置等),以便在函数调用、返回以及异常处置惩罚等情况下,能够精确规复和继续执行代码。
可以说,帧对象为代码对象的执行提供了一个动态的上下文情况,它随着代码的执行而创建和销毁,形成一个调用栈,反映了函数调用的条理结构。
其相关界说在源码文件:cpython/Include/internal/pycore_frame.h- struct _frame {
- PyObject_HEAD
- PyFrameObject *f_back; /* previous frame, or NULL */
- struct _PyInterpreterFrame *f_frame; /* points to the frame data */
- PyObject *f_trace; /* Trace function */
- int f_lineno; /* Current line number. Only valid if non-zero */
- char f_trace_lines; /* Emit per-line trace events? */
- char f_trace_opcodes; /* Emit per-opcode trace events? */
- PyObject *f_extra_locals; /* Dict for locals set by users using f_locals, could be NULL */
- /* This is purely for backwards compatibility for PyEval_GetLocals.
- PyEval_GetLocals requires a borrowed reference so the actual reference
- is stored here */
- PyObject *f_locals_cache;
- /* The frame data, if this frame object owns the frame */
- PyObject *_f_frame_data[1];
- };
复制代码 当调用函数时,会创建一个新的帧对象并压入调用栈。
当函数执行竣事返回时,该帧对象从调用栈中弹出,假造机根据帧对象中记载的f_back(指向前一个帧对象的引用)等信息,规复到调用函数之前的状态,继续执行后续代码。
4. 总结
本文重要对 CPython 执行 Python 步调的过程做一个初步的宏观介绍,了解了其重要的阶段和核心概念。
后续打算进一步就CPython某个部门的具体实现细节来介绍,徐徐对CPython的内部机制进行深入的了解。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |