『Python底层原理』--CPython 虚拟机

Python 编程的世界里,我们每天都在使用 python 命令运行程序,但你是否曾好奇这背后究竟发生了什么?

本文将初步探究 CPythonPython 中最流行的实现)的一些内部机制,为了更好的来理解 Python 语言的底层运作。

1. CPython 简介

CPython 是用** C 语言**编写的 Python 解释器,在众多 Python 实现(如 PyPyJythonIronPython 等)中,它以其原创性、良好的维护性和高人气脱颖而出。

了解 CPython 的一些内部机制,对我们学习和使用Python语言本身也有很大的帮助:

  1. 有助于深入理解 Python 语言:了解实现细节能让我们更轻松地掌握 Python 的一些特性
  2. 实现细节在实际应用中至关重要:对象存储方式、垃圾回收机制以及多线程协调等方面的知识,对于理解语言的适用性、局限性、性能评估和效率检测都非常关键
  3. 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代码经过编译之后的字节码是可以查看的,比如下面简单写一个加法函数。

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

c 复制代码
#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函数体创建一个代码对象。

在这个代码对象中,字节码部分记录了如何加载变量xy、执行加法操作返回结果等指令序列。

同时,还包含co_argcount(参数数量,此处为 2)等属性,这些属性描述了函数的参数使用情况。

此外,代码对象还会记录函数定义所在的文件名(co_filename )、起始行号(co_firstlineno)等信息,方便调试和代码分析。

3.2. 函数对象

函数对象 不仅仅包含可执行代码(即代码对象),还存储了与函数相关的其他重要信息,如函数名文档字符串docstring)、默认参数外部作用域变量值等。

函数对象将代码对象与函数运行所需的上下文信息整合在一起,使得函数可以在不同的环境中被正确调用和执行。

多个函数对象 可以引用同一个代码对象,通过不同的外部信息实现不同的功能,例如闭包的实现。

其相关定义在源码文件:cpython/Include/cpython/funcobject.h

c 复制代码
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

c 复制代码
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的内部机制进行深入的了解。

相关推荐
精灵vector22 分钟前
构建专家级SQL Agent交互
python·aigc·ai编程
Zonda要好好学习37 分钟前
Python入门Day2
开发语言·python
Vertira39 分钟前
pdf 合并 python实现(已解决)
前端·python·pdf
太凉43 分钟前
Python之 sorted() 函数的基本语法
python
项目題供诗1 小时前
黑马python(二十四)
开发语言·python
晓13132 小时前
OpenCV篇——项目(二)OCR文档扫描
人工智能·python·opencv·pycharm·ocr
是小王同学啊~2 小时前
(LangChain)RAG系统链路向量检索器之Retrievers(五)
python·算法·langchain
AIGC包拥它2 小时前
提示技术系列——链式提示
人工智能·python·langchain·prompt
孟陬2 小时前
Python matplotlib 如何**同时**展示正文和 emoji
python
何双新2 小时前
第 1 课:Flask 简介与环境配置(Markdown 教案)
后端·python·flask