实验:CPython 迭代协议底层验证(`PyObject_GetIter` / `PyIter_Next`)

目标:用 C 扩展复刻 while item := next(it): 的底层实现,适配 MSYS2 MinGW64 + Python 3.14,避开嵌入模式 WinMain 链接坑。

环境:MINGW64,工作目录 /d/cpp/cpython/Mac

1. 文件清单(Mac 目录下一共 3 个文件)

复制代码
Mac/
├─ iter_mod.c        # C扩展源码,调用CPython迭代API
├─ setup.py          # setuptools编译脚本
└─ test_ext.py       # Python测试调用脚本

2. 各个文件完整源码

iter_mod.c

复制代码
#include <Python.h>

static PyObject* iter_demo(PyObject* self, PyObject* args)
{
    PyObject *iterable;
    if (!PyArg_ParseTuple(args, "O", &iterable))
    {
        return NULL;
    }
    PyObject *it = PyObject_GetIter(iterable);
    if (!it)
        return NULL;

    PyObject *item;
    int has_data = 0;
    while ((item = PyIter_Next(it)) != NULL)
    {
        has_data = 1;
        PyObject_Print(item, stdout, 0);
        putchar('\n');
        Py_DECREF(item);
    }
    // PyIter_Next 返回NULL:区分 StopIteration 和其他异常
    if (PyErr_Occurred())
    {
        if (PyErr_ExceptionMatches(PyExc_StopIteration))
        {
            PyErr_Clear();
        }
        else
        {
            Py_DECREF(it);
            return NULL;
        }
    }
    Py_DECREF(it);

    if (!has_data)
    {
        printf("Iterable is empty\n");
    }
    Py_RETURN_NONE;
}

static PyMethodDef IterMethods[] = {
    {"iter_demo", iter_demo, METH_VARARGS, "Demo PyIter_Next, same as while item:=next(it)"},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef itermodule = {
    PyModuleDef_HEAD_INIT,
    "iter_mod",
    NULL,
    -1,
    IterMethods
};

PyMODINIT_FUNC PyInit_iter_mod(void)
{
    return PyModule_Create(&itermodule);
}

setup.py

复制代码
from setuptools import setup, Extension

module = Extension("iter_mod", sources=["iter_mod.c"])
setup(
    name="iter_mod",
    ext_modules=[module]
)

test_ext.py

复制代码
import iter_mod

lst1 = [1,2,3]
iter_mod.iter_demo(lst1)

print("----")
lst2 = []
iter_mod.iter_demo(lst2)

3. 完整操作步骤(MINGW64 终端,顺序执行)

  1. 进入工作目录

    cd /d/cpp/cpython/Mac

  2. 安装依赖 setuptools(只需要执行一次)

    pacman -S mingw-w64-x86_64-python-setuptools

  3. 编译 C 扩展模块,自动生成 .pyd

    python3 setup.py build_ext --inplace

编译成功后,目录出现文件:iter_mod.cp314-mingw_x86_64_msvcrt_gnu.pyd

  1. 运行测试脚本

    python3 test_ext.py

4. 预期输出

复制代码
1
2
3
----
Iterable is empty

5. 原理对照(《CPython 设计与实现》)

表格

C API 等价 Python 语义
PyObject_GetIter(obj) it = iter(obj)
PyIter_Next(it) item = next(it)

C 里的循环:

复制代码
while ((item = PyIter_Next(it)) != NULL)
{
    // 处理元素
}

等价 Python 伪代码:

复制代码
it = iter(seq)
while True:
    try:
        item = next(it)
    except StopIteration:
        break
    # 处理元素

PyIter_Next 返回 NULL 时:

  • 如果异常是 StopIteration:代表迭代耗尽,清除异常、退出循环;
  • 如果是别的异常:直接向上抛出。

6. 补充说明

  • 方案选型说明:放弃【C 主程序嵌入 Python 解释器(Py_Initialize)】。MSYS2 Python 3.14 的导入库内置 GUI 子系统标记,会强制寻找 WinMain,链接难以绕过;
  • C 扩展方案:Python 进程先启动,再加载我们写的 C 代码,属于 CPython 官方推荐的扩展开发方式,无子系统链接冲突,适合底层 API 实验;
  • 每次修改 iter_mod.c,都需要重新执行 python3 setup.py build_ext --inplace 重新编译。

7. 后续可选拓展方向

  1. 自定义可迭代对象、自定义迭代器,实现 __iter__ / __next__ 协议;
  2. 构造会抛出业务异常的迭代器,观察 PyIter_Next 的异常传递;
  3. 配置 VS Code + GDB,断点跟踪进入 PyIter_Next 内部源码调试;
  4. 封装接口,完全对齐 while item := next(it): 的调用形式。

哈哈没事!咱们把它压到最简,只抓最核心的,不讲复杂名词。

✅ 一句话总结我们刚刚做成的事

写了一段 C 代码,调用 Python 底层原生函数,模拟:

复制代码
it = iter([1,2,3])
while True:
    try:
        item = next(it)
        print(item)
    except StopIteration:
        break

也就是书上那句:while item := next(it):

✅ 记住两个最关键的对应关系就够

  • PyObject_GetIter = iter(对象)
  • PyIter_Next = next(迭代器)

✅ 最简操作清单(以后想用直接复制跑)

复制代码
cd /d/cpp/cpython/Mac
python3 setup.py build_ext --inplace
python3 test_ext.py

改了 C 代码,就重新跑一遍编译那句。

✅ 剩下的那些:

Py_DECREF、异常清理、setuptools、pyd、ABI、subsystem、WinMain......

暂时不用硬啃,先当成"配套工具胶水",知道它负责不出错就行。

我们慢慢来,想玩哪个小实验,我们就单独拆一小块,少一点文字,一步一步看效果 😊

要不要先就停在这里,或者我们换个更简单的小实验?

一句话大白话

  • CMD:Windows 原生自带的老命令壳子(Windows 自家)
  • MINGW64(MSYS2) :在 Windows 里套了一个 Bash(Linux 风格终端) ,附带 GCC 编译器、Linux 那一套命令(ls、gcc、pacman 包管理器),专门用来编译 C/C++

简单对比

表格

项目 CMD(Windows 原生) MINGW64(MSYS2 里的 bash+mintty)
是谁 Windows 自带,DOS 传承下来 跑的是 bash(Linux 常用 shell),窗口程序叫 mintty(就是你截图这个窗口)MSYS2
路径写法 D:\cpp\cpython 反斜杠 \ /d/cpp/cpython 斜杠 /,和 Linux 一样
列出文件 dir ls
编译 C 语言 默认没有 gcc,要自己折腾 自带 gcc、g++,就是我们刚才编译 C 扩展用的
命令语法 Windows 一套:copy del Linux 风格:cp rm mkdir
包管理 没有 有 pacman,像 Arch Linux,可以一键装软件(刚才装 setuptools 就是 pacman)
适合干什么 简单 Windows 管理、批处理 .bat C/C++ 开发、跑 GNU 工具、编译开源代码(我们现在 CPython 底层实验就用它)

几个很实用的小重点

  1. MINGW64 编译出来的 exe/pyd,依然是原生 Windows 程序,不是虚拟机,不用额外 dll 就能在普通 Windows 上跑这点很重要。MSYS22
  2. 两者命令不能直接混用
    • cmd 里敲 ls → 报错
    • MINGW64 bash 里敲 dir 也不行(除非额外装兼容工具)
  3. 你截图这个 mintty 只是窗口外壳,里面跑的真正解释命令的程是 bash。MSYS22
  4. 我们刚才做的 C 扩展实验,必须在 MINGW64 里面做,因为要它里面的 gcc、python 环境、编译链;放到普通 cmd 里直接找不到 gcc

CPython 和 C 语言核心技术的关系(深度进阶版)

一句话总结:CPython 本身就是用 C 写的一套庞大程序,它是 C 语言工程的巅峰产物;但 Python 语法本身和 C 完全无关。

拆开讲清楚:

  1. 核心定义:实现语言 vs 被实现语言
    我们从官网下载的标准 Python(CPython),整个解释器、虚拟机、对象模型、内存管理、GIL、内置类型(int/list/dict)、内置函数,全部都是 C 代码写出来的。
    你写的 Python 源码(.py)只是一份文本,它本身不是 C;是这个由 C 编译出来的程序(python.exe),读懂了这份文本并执行它。

大神的理解:

C 是实现语言(建造汽车的机器和材料);Python 是被实现的语言(汽车的设计图纸和驾驶规则)。

(你的实战印证):咱们编译出的那个 python.exe,就是这几万行 C 代码结合在一起的终极机器。

  1. CPython 大量依赖的 C 语言核心技术
    这不仅是理论,更是你亲手在 VS Code 里看到的现实:

C 的内存管理(malloc/free 与对象池)

CPython 的对象池、引用计数、分代垃圾回收,底层都是封装 C 的 malloc / free。所有 Python 对象(哪怕是一个数字 1),在底层都是一个 C 结构体 PyObject*。

c 复制代码
c
// 源码里的基础对象头(你在 typeobject.c 里亲眼见过的)
typedef struct _object {
    Py_ssize_t ob_refcnt;  // 引用计数(垃圾回收的核心)
    struct _typeobject *ob_type; // 指向类型对象的指针
} PyObject;

C 的指针(无处不在的 PyObject*)

整个 CPython 到处是指针,所有对象都靠 PyObject* 操作,这是 C 语言操作内存的核心。

(你的实战印证):你在调试 PyUnicode_InternInPlace 时,左侧变量面板里那个 p = 0x0000003ed1fef0b0,就是纯正的 C 语言指针内存地址。

C 的结构体与宏(宏定义魔法)

CPython 大量使用 C 结构体来构建 Python 的类型系统,并且大量使用宏(Macro)来优化性能。你在阅读源码时看到的那些以 Py_ 或 _Py 开头的,很多都是宏,它们会在编译时被直接替换成 C 代码,以减少函数调用的开销。

C 的函数指针(多态的实现)

Python 为什么能实现面向对象和"鸭子类型"?底层其实是 C 语言的函数指针。每个类型对象(比如 PyUnicode_Type)内部都装满了指向具体 C 函数的指针(如 tp_dealloc, tp_repr)。当你在 Python 里调用 str.upper() 时,虚拟机就是顺着函数指针找到了对应的 C 函数。

C 的编译与链接

CPython 源码要通过 msvc/gcc 编译成二进制可执行文件。我们写的 C 扩展模块(.pyd / .so),也是 C 代码编译后,链接进 CPython 虚拟机,给 Python 调用的。

  1. 边界:Python 代码 ≠ C 代码
    你写的 a = 1; print(a) 这种 Python 代码,不是 C,不会直接编译成机器码;

是 CPython(C 写的虚拟机)把它转成中间产物------字节码(Bytecode),再由 C 语言写的巨型 switch-case 循环(ceval.c 中的 _PyEval_EvalFrameDefault)逐条解释执行。

(你的实战印证):你之前用 dis 模块看到的 LOAD_CONST 和 FORMAT_VALUE,就是字节码。C 虚拟机就是靠读懂这些字节码干活的。

只有 CPython 解释器本身、内置模块、C 扩展,才是 C。

  1. 串起核心痛点:GIL(全局解释器锁)

    GIL 就是 C 代码里的一个互斥锁(Windows 下是 CRITICAL_SECTION,Linux 下是 pthread_mutex),它是操作系统底层的 C 多线程原语封装出来的。

    因为 CPython 的内存管理(引用计数 ob_refcnt)在 C 层面不是线程安全的,为了防止多个 C 线程同时修改同一个对象的引用计数导致内存崩盘,CPython 的开发者硬生生加了一把大锁。

    这也是为什么 GIL 是 CPython 的痛点:它是 C 层实现的锁,锁住了整个虚拟机的解释执行过程。

  2. 横向对比:为什么 PyPy 是另一个物种?

    CPython:C 写的解释器。提供 Python 语法、虚拟机、运行时。

PyPy:不是 C 写的(主体是用 Python 的子集 RPython 写的),它自带 JIT(即时编译)技术,能把高频执行的 Python 代码直接编译成机器码。所以 PyPy 跑纯 Python 循环极快,但它的 C 扩展兼容性不如 CPython。

🌟 终极补充:如果把视角拔高(更高维的认知)

你之前学的 f-string 对比、深浅拷贝、字符串驻留,其实全都可以在这个框架里找到答案:

关于变量:在 Python 层面,a = "hello" 是把标签贴在对象上;在 C 层面,其实是 C 在内存里分配了一块 PyUnicodeObject 结构体,然后把这块内存的指针赋值给了变量 a。

关于性能:为什么 f-string 比 % 快?因为在 C 语言的底层,Python 3.6+ 对 f-string 的字节码做了极致的特化(FORMAT_VALUE),减少了 C 函数调用的次数。

关于深浅拷贝:在 C 语言层面,浅拷贝只是拷贝了外层的 C 数组指针;深拷贝则是递归地去 malloc 新的内存,把里面的对象也复制了一遍。

小结

✅ 关系:CPython 依托 C 语言的底层能力(指针、内存、编译、系统调用)造出来的 Python 运行时;C 是它的基石。

❌ 误区:Python 不是 C 的语法糖,Python 代码不会自动变成 C 代码。

💡 进阶:搞懂了 C 和 CPython 的关系,你就懂了 Python 为什么会"慢"(解释执行),以及为什么"有些扩展库(如 NumPy)那么快"(底层全是 C 优化)。

相关推荐
DevLoom_10 小时前
macOS 命令行 curl/git 报 SSL 证书错误?eMudhra 事件完整修复指南
git·macos·ssl
清水白石00816 小时前
Python 安装与环境配置:Windows、macOS、Linux 完整指南(第2集)
windows·python·macos
hhhjhl16 小时前
开源鸿蒙平台 CMP 三方库「compose-macos-26-ui」适配全流程
macos·开源·harmonyos
七牛云行业应用1 天前
DeepSeek Harness(dsh)安装完整指南:Windows、macOS、Linux 三平台 + 桌面端与 Web 端全覆盖
linux·windows·macos·ai·ai编程
全栈探索者2 天前
一枚 162MB 的挂件,改写一只猫的画风:LoRA 实战——风格定制与人脸保持
人工智能·macos·lora·aigc·stablediffusion·comfyui
福大大架构师每日一题2 天前
ollama v0.40.11发布:macOS原生语音生成上线,d3决策模型正式加入
macos·策略模式
小天源2 天前
智叟云集系统:便携部署、MAC 授权与交付排障
macos
Joy T2 天前
voice-dump-to-todo:把混乱的语音整理成可执行待办,一个实用的 Agent Skill
ide·macos·语音识别·xcode·任务管理·自研skill
JPower_mr.g2 天前
SmartCall v1.0.9 发布:智能体知识库上线,让 AI 客服先查资料再回答
java·ide·人工智能·macos·架构·开源·xcode