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

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

1. CPython 简介

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

了解 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代码经过编译之后的字节码是可以查看的,比如下面简单写一个加法函数。

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

『Python底层原理』--CPython 虚拟机的更多相关文章

  1. 『Python基础-1 』 编程语言Python的基础背景知识

    #『Python基础-1 』 编程语言Python的基础背景知识 目录: 1.编程语言 1.1 什么是编程语言 1.2 编程语言的种类 1.3 常见的编程语言 1.4 编译型语言和解释型语言的对比 2 ...

  2. 『Python基础-12』各种推导式(列表推导式、字典推导式、集合推导式)

    # 『Python基础-12』各种推导式(列表推导式.字典推导式.集合推导式) 推导式comprehensions(又称解析式),是Python的一种独有特性.推导式是可以从一个数据序列构建另一个新的 ...

  3. 『Python基础-11』集合 (set)

    # 『Python基础-11』集合 (set) 目录: 集合的基本知识 集合的创建 访问集合里的值 向集合set增加元素 移除集合中的元素 集合set的运算 1. 集合的基本知识 集合(set)是一个 ...

  4. 『Python基础-10』字典

    # 『Python基础-10』字典 目录: 1.字典基本概念 2.字典键(key)的特性 3.字典的创建 4-7.字典的增删改查 8.遍历字典 1. 字典的基本概念 字典一种key - value 的 ...

  5. 『Python基础-9』元祖 (tuple)

    『Python基础-9』元祖 (tuple) 目录: 元祖的基本概念 创建元祖 将列表转化为元组 查询元组 更新元组 删除元组 1. 元祖的基本概念 元祖可以理解为,不可变的列表 元祖使用小括号括起所 ...

  6. 『Python基础-8』列表

    『Python基础-8』列表 1. 列表的基本概念 列表让你能够在一个地方存储成组的信息,其中可以只包含几个 元素,也可以包含数百万个元素. 列表由一系列按特定顺序排列的元素组成.你可以创建包含字母表 ...

  7. 『Python基础-7』for循环 & while循环

    『Python基础-7』for循环 & while循环 目录: 循环语句 for循环 while循环 循环的控制语句: break,continue,pass for...else 和 whi ...

  8. 『Python基础-6』if语句, if-else语句

    # 『Python基础-6』if语句, if-else语句 目录: 条件测试 if语句 if-else语句 1. 条件测试 每条if语句的核心都是一个值为True或False的表达式,这种表达式被称为 ...

  9. 『Python基础-5』数字,运算,转换

    『Python基础-5』数字,运算,转换 目录 基本的数字类型 二进制,八进制,十六进制 数字类型间的转换 数字运算 1. 数字类型 Python 数字数据类型用于存储数学上的值,比如整数.浮点数.复 ...

  10. 『Python基础-4』字符串

    # 『Python基础-4』字符串 目录 1.什么是字符串 2.修改字符串 2.1 修改字符串大小 2.2 合并(拼接)字符串 2.3 使用乘号'*'来实现字符串的叠加效果. 2.4 在字符串中添加空 ...

随机推荐

  1. ARC134C The Majority

    ARC134C The Majority link:[ARC134C] The Majority 小清新数学题.(反正我做不出来) 简要题意 有\(K\)个箱子,编号为\(1\)到\(K\)的箱子.起 ...

  2. rust 终端输出 debug 信息

    配置方法 将 env_logger log 添加到 Cargo.toml : 打开 Cargo.toml 文件并在 [dependencies] 部分下添加 env_logger log . [pac ...

  3. java公式解析器学习与开发(2)——前缀表达式

    释义 前缀表达式就是前序表达式. 前缀表达式就是不含括号的算术表达式,而且它是将运算符写在前面,操作数写在后面的表达式,为纪念其发明者波兰数学家Jan Lukasiewicz也称为"波兰式& ...

  4. golang之热加载Fresh&air

    Fresh 是一个命令行工具,每次保存Go或模版文件时,该工具都会生成或重新启动Web应用程序.Fresh将监视文件事件,并且每次创建/修改/删除文件时,Fresh都会生成并重新启动应用程序.如果go ...

  5. python之日志记录loguru

    安装: pip install loguru 基础使用: from loguru import logger logger.debug("This is a debug...") ...

  6. 基于 MongoTemplate 实现MongoDB的复杂查询

    MongoDB是典型的非关系型数据库,但是它的功能越来越复杂,很多项目中,我们为了快速拓展,甚至直接使用Mongo 来替代传统DB做数据持久化.虽然MongoDB在支持具体业务时没有问题,但是由于它是 ...

  7. 记ios的input框获取焦点之后界面放大问题

    在移动端开发项目中,发现页面在使用 iPhone 访问的时候,点击 input 和 textarea 等文本输入框聚焦 focus() 时,页面会整体放大,而且失去焦点之后页面不能返回原来的样子.检查 ...

  8. uniapp安卓在线更新版本

    实现逻辑 通过获取线上的版本号和app的版本号进行对比 查看是不是最新版 - app版本号小于线上版本号则不是最新版 提示更新 模拟检测更新请求 起一个服务,也就是检测更新的接口 返回值为最新版本号和 ...

  9. OpenEuler文件被锁定的解决方法|网卡修改不生效的解决办法

    欧拉系统(含centos等linux系统)修改文件,一直提示readonly,不让改.原因有可能是这个文件给锁定了. 解决方法: 使用以下两个命令: • chattr 改变文件属性 • lsattr ...

  10. 2024年1月Java项目开发指南2:项目设计

    确定软件架构 1.前端用什么技术,什么框架,什么版本 2.后端用什么技术,什么框架,什么版本 3.用些什么软件,软件版本? 比如: 前端:vue3 + Element-Plus + Axios 后端: ...