杰瑞科技汇

Python源码如何深入解析?

第一部分:准备工作

在深入源码之前,你需要做好以下准备:

Python源码如何深入解析?-图1
(图片来源网络,侵删)

获取 Python 源码

最权威的源码仓库是 GitHub 上的 cpython

# 克隆最新的 CPython 源码
git clone https://github.com/python/cpython.git

你也可以选择下载特定版本的源码压缩包,这在你研究某个特定 Python 版本时非常有用。

必备的开发环境

  • C 编译器: Python 是用 C 语言编写的,所以你需要一个 C 编译器(如 GCC, Clang, MSVC)来编译和调试源码。
  • 构建工具: 主要是 make (在 Linux/macOS 上) 或 PCBuild (在 Windows 上)。
  • 调试器: 强烈推荐 GDB (Linux/macOS) 或 Visual Studio Debugger (Windows),没有调试器,阅读源码会事倍功半。
  • Python 环境: 你需要一个已经安装好的 Python 版本来运行 make 等构建脚本。

编译和安装 Python 源码

进入 cpython 目录,执行以下步骤:

# 1. 创建一个构建目录
mkdir build
cd build
# 2. 配置编译选项 (可以不加参数,使用默认配置)
../configure --with-pydebug  # --with-pydebug 会增加很多调试信息,比如对象引用计数,强烈推荐!
# 3. 编译
# -j 后面跟你的 CPU 核心数,可以加快编译速度
make -j 4
# 4. 安装 (可选,推荐)
# 这会创建一个局部的 Python 环境,不会影响你系统自带的 Python
make install

编译完成后,你会在 build 目录下得到一个可执行的 python 文件,运行它,你就得到了一个由你自己编译的、可调试的 Python 解释器。

Python源码如何深入解析?-图2
(图片来源网络,侵删)

第二部分:阅读源码的核心知识

Python 源码的核心是 CPython,理解以下几个关键概念至关重要:

Python 是如何运行的?

  1. 源代码 (.py): 你写的 Python 代码。
  2. 解释器: 将源代码转换成计算机能理解的指令。
  3. 字节码 (.pyc): 解释器将源代码编译成的一种中间、平台无关的指令集,它比源代码更接近机器码,执行效率更高。
  4. 虚拟机: 一个抽象的计算机,专门用来执行字节码,CPython 的虚拟机通常被称为 PVM (Python Virtual Machine)
  5. C 扩展: Python 的很多内置函数和标准库是用 C 语言实现的,它们直接作为 PVM 的指令存在,效率极高。

流程图: 你的代码 (.py) -> 编译器 -> 字节码 (.pyc) -> PVM (解释器核心) -> 机器码

你的 .py 文件首先被编译成字节码,然后由 PVM 逐行执行,理解字节码和 PVM 是理解 Python 运行机制的关键。

关键数据结构

  • PyObject: 所有 Python 对象的“祖先”,它是一个 C 结构体,包含了对象最核心的信息:引用计数类型指针
    typedef struct _object {
        PyObject_HEAD
    } PyObject;

    PyObject_HEAD 是一个宏,通常会展开为:

    Python源码如何深入解析?-图3
    (图片来源网络,侵删)
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt; // 引用计数
    struct _typeobject *ob_type; // 指向该对象类型的指针
  • PyTypeObject: 定义了“类型”本身,int, str, list 等,它是一个巨大的结构体,包含了创建该类型对象所需的所有信息,
    • 对象的大小 (tp_basicsize)
    • 析构函数 (tp_dealloc)
    • 打印函数 (tp_print)
    • 哈希函数 (tp_hash)
    • 方法表 (tp_methods): 这是最重要的部分之一,定义了该类型支持的所有方法(如 list.append)。

引用计数

这是 CPython 内存管理的基石,每个 PyObject 都有一个 ob_refcnt,记录了有多少个地方引用了这个对象。

  • 引用增加: Py_INCREF(obj)
  • 引用减少: Py_DECREF(obj)
  • 当引用计数归零时: Py_DECREF 会发现计数为 0,于是调用该对象的析构函数 (tp_dealloc),释放内存。

注意: CPython 后来引入了 GIL (全局解释器锁)分代垃圾回收 作为引用计数的补充,以处理循环引用等问题。


第三部分:源码结构解读

CPython 的源码目录结构非常清晰:

cpython/
├── Include/          # 所有 C 头文件 (.h),定义了 Python API 和数据结构
│   ├── Python.h     # 核心头文件,几乎所有 C 扩展都会包含它
│   └── ...
├── Objects/          # 核心对象的实现,如 int, str, list, dict, function 等
│   ├── intobject.c  # int 类型的具体实现
│   ├── listobject.c # list 类型的具体实现
│   └── ...
├── Python/          # 解释器的核心代码,包括字节码编译和 PVM
│   ├── ast.c        # 抽象语法树 的构建
│   ├── compile.c    # 将 AST 编译成字节码
│   ├── frameobject.c # 帧 对象,即执行栈帧
│   └── ceval.c      # **最核心的文件!** PVM 的主循环,执行字节码
├── Modules/         # 标准库中用 C 实现的模块,如 `os`, `sys`, `json` 等
├── Parser/          # Python 词法分析器和语法分析器的实现
├── Grammar/         # Python 语言的语法文件 (`.pgen` 文件)
└── ...

第四部分:实战演练:追踪一个函数的执行

让我们以最简单的 print("Hello") 为例,看看它在源码中是如何一步步执行的。

步骤 1: 字节码层面

我们来看 print("Hello") 被编译成了什么字节码,使用我们编译好的 Python:

# 在 cpython/build 目录下
./python -c "import dis; dis.dis(print('Hello'))"

输出类似这样(版本不同可能略有差异):

  1           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Hello')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

关键指令是 CALL_FUNCTION,CPython 的虚拟机(在 ceval.c 中)就是通过执行这些指令来运行代码的。

步骤 2: C 层面追踪

我们用 GDB 来调试,我们的目标是:当执行 CALL_FUNCTION 时,看看 C 层面发生了什么。

  1. 准备一个测试文件:

    # test.py
    print("Hello")
  2. 启动 GDB:

    # 使用我们编译的 python,并加载调试信息
    gdb ./python
  3. 在 GDB 中设置断点:

    (gdb) b ceval.c:PyEval_EvalFrameEx
    Breakpoint 1 at 0x5555557b5a20: file ceval.c, line 1947.
    (gdb) r test.py

    PyEval_EvalFrameExceval.c 中的核心函数,负责执行一个帧对象里的字节码。

  4. 单步执行: 程序会在 PyEval_EvalFrameEx 函数的开头停下,使用 n (next) 或 s (step) 命令来单步执行,你可以通过 x/i $pc 查看当前正在执行的 C 指令,或者通过 info registers 查看寄存器状态。

    当你执行到 CALL_FUNCTION 对应的 C 代码时,你会看到类似这样的逻辑(简化版):

    • 从操作数栈中弹出函数对象 (print) 和参数 ('Hello')。
    • 检查函数对象的类型,发现它是一个 Python 的 built-in function (BIF)。
    • 调用 builtin_print 函数(这个函数定义在 Modules/main.cModules/builtinmodule.c 中,具体取决于 Python 版本)。
    • builtin_print 函数接收参数,然后调用 C 的 printf 或类似函数将 "Hello" 打印到控制台。

步骤 3: 查找 print 的实现

print 为什么能直接用?因为它是一个内置函数,它的实现就在 Modules/builtinmodule.c 文件中,搜索 PyDoc_STRVAR(print_doc)PyDoc_STRVAR(print_function_docstring),你就能找到 static PyMethodDef builtin_print[] 的定义,这个 PyMethodDef 数组就是 print 函数的 C 实现。


第五部分:探索路径建议

  1. list 开始: list 是 Python 中最常用的数据结构之一,它的源码在 Objects/listobject.c

    • 目标: 理解 list.append 是如何工作的。
    • 方法: 在 listobject.c 中搜索 append 函数,看看它如何检查边界、如何重新分配内存、如何增加元素,你会发现它操作的是一个 C 数组 (listobject->ob_item)。
  2. 深入 dict: dict (字典) 的实现非常精妙,从 CPython 3.6 开始,其底层数据结构从“哈希表+冲突链表”改为了“哈希表+冲突开放寻址法”。

    • 目标: 理解 dict 的查找、插入和删除操作。
    • 方法: 源码在 Objects/dictobject.c,重点看 lookdict 函数(查找键)和 dictinsert 函数(插入键值对),理解 PyDictKeyEntry 结构和 dk_indices 数组的作用。
  3. 理解 intfloat: 它们是如何表示的?为什么 int 在 Python 3 中可以无限大?

    • 目标: 理解 Python 数值类型的内部表示。
    • 方法: 源码在 Objects/longobject.c (Python int 的实际类型是 PyLongObject) 和 Objects/floatobject.c,你会发现 Python 的 int 是一个变长的 C 数组,用来模拟大整数。
  4. 探索 GIL: GIL 是 CPython 的一个核心特性。

    • 目标: 理解 GIL 是什么,它如何工作,以及它带来的影响。
    • 方法: 源码在 Python/ceval.c 中搜索 PyThread_acquire_lockPyThread_release_lock,你会发现 GIL 的获取和释放是在解释器的主循环中控制的。

第六部分:实用技巧和资源

  • 善用搜索: grepGitHub 的代码搜索是你的好朋友,当你想知道某个函数在哪里定义时,直接搜索函数名。
  • 阅读文档: Python 的 Developer's GuideC API Reference 是官方权威资料。
  • 社区资源:
    • "Writing an Extension Module in C": 这篇官方教程是理解 CPython C API 的最佳起点。
    • "Python 内核剖析": 豆瓣上有这本书的中文版,是经典读物。
    • PyCon 视频: YouTube 上有很多关于 Python 内部机制的精彩演讲,"The GIL in Depth" 等。
  • 不要害怕: 源码量巨大,不可能一次性全部读完,从一个你最感兴趣、最常用的点开始,像剥洋葱一样一层层深入。

保持好奇心和耐心。 每当你对 Python 的某个行为感到困惑时,这恰恰是你深入源码的最好机会,祝你在探索 Python 源码的旅程中玩得开心!

分享:
扫描分享到社交APP
上一篇
下一篇