首页
/ CPython C API Mapping 协议详解:PyMapping_* 函数族完整参考与源码级实现剖析

CPython C API Mapping 协议详解:PyMapping_* 函数族完整参考与源码级实现剖析

2026-09-04 10:44:21作者:董灵辛Dennis

本文以 CPython 官方文档 Mapping Protocol 为核心,系统讲解 C API 中全部 12 个 PyMapping_* 函数的语义、返回值约定与版本演进,并结合 Objects/abstract.cInclude/abstract.h 的源码实现,说明这些函数如何通过类型槽 tp_as_mappingPyMappingMethods 结构落地,帮助你在 C 扩展中安全地读取、写入、探测和遍历任意 Python 映射对象(字典、自定义 __getitem__ 类等)。

一、Mapping 协议是什么:类型槽与 PyMappingMethods

在 CPython 中,"mapping 协议"由类型对象上的一个槽位承载。Include/cpython/object.h 定义了 PyMappingMethods 结构(L122-L126):

typedef struct {
    lenfunc mp_length;            /* 对应 __len__ */
    binaryfunc mp_subscript;     /* 对应 __getitem__ */
    objobjargproc mp_ass_subscript; /* 对应 __setitem__/__delitem__ */
} PyMappingMethods;

该结构通过 struct _typeobjecttp_as_mapping 成员挂载到每个类型上(Include/cpython/object.h L167)。只要类型的 tp_as_mapping->mp_subscript 非空,o[key] 这类下标表达式就能工作;而 PyMapping_Check() 正是以"是否注册了 mp_subscript"作为判定标准(Objects/abstract.c L2268-L2273):

int
PyMapping_Check(PyObject *o)
{
    return o && Py_TYPE(o)->tp_as_mapping &&
        Py_TYPE(o)->tp_as_mapping->mp_subscript;
}

由此可以确认两点语义:

  • PyMapping_Check() 对"拥有 __getitem__ 方法的 Python 类"也返回 1——因为无法预判该类的键类型,它只是表明对象"可能"提供 mapping 能力;
  • 该函数永不失败(对 NULL 返回 0),可作为无副作用的前置检查。

文档还提示了与通用 API 的对应关系::c:func:PyObject_GetItem:c:func:PyObject_SetItem:c:func:PyObject_DelItem`` 是 mapping 读写的最底层入口,PyMapping_* 系列大多是在其之上加了字符串键转换或错误处理策略的便捷封装。

二、API 速查表

函数 作用 返回值 引入/变更
PyMapping_Check(o) 是否提供 mapping 协议或切片支持 1 / 0,永不失败 既有
PyMapping_Size(o) / PyMapping_Length(o) 等价于 len(o) 成功返回键数量,失败返回 -1 既有
PyMapping_GetItemString(o, key) 以 UTF-8 const char* 键取值,等价于 o[key] 值(新引用)/ 失败 NULL 既有
PyMapping_GetOptionalItem(obj, key, &result) 找不到键不抛 KeyErrorPyObject_GetItem 1=找到,0=未找到,-1=其他错误 3.13 新增
PyMapping_GetOptionalItemString(obj, key, &result) 同上,键为 const char* 同上 3.13 新增
PyMapping_SetItemString(o, key, v) 以字符串键赋值,等价于 o[key]=v 0 成功 / -1 失败 既有
PyMapping_DelItem(o, key) PyObject_DelItem 的别名,等价于 del o[key] 0 成功 / -1 失败 既有
PyMapping_DelItemString(o, key) 以字符串键删除 0 成功 / -1 失败 既有
PyMapping_HasKeyWithError(o, key) 等价于 key in o,保留异常 1 / 0 / 失败 -1 3.13 新增
PyMapping_HasKeyStringWithError(o, key) 同上,键为 const char* 1 / 0 / 失败 -1 3.13 新增
PyMapping_HasKey(o, key) 等价于 key in o,静默吞掉所有异常 1 / 0,永不失败 既有
PyMapping_HasKeyString(o, key) 同上,键为 const char* 1 / 0,永不失败 既有
PyMapping_Keys(o) / PyMapping_Values(o) / PyMapping_Items(o) 返回键/值/键值对列表 列表(新引用)/ 失败 NULL 3.7 起恒定返回列表

三、读取键值:PyMapping_GetItemString 与 3.13 新增的 Optional 变体

3.1 字符串键的取值与赋值

PyMapping_GetItemString(o, key)PyObject_GetItem 的区别仅在于键以 UTF-8 编码的 const char* 传入。其实现(Objects/abstract.c L2307-L2322)先经 PyUnicode_FromString 构造临时 str 对象,再委托给 PyObject_GetItem,用完即 Py_DECREF——这就是所有 *String 变体统一的"造键→调用→释放"模式:

PyObject *
PyMapping_GetItemString(PyObject *o, const char *key)
{
    PyObject *okey, *r;
    if (key == NULL) {
        return null_error();
    }
    okey = PyUnicode_FromString(key);
    if (okey == NULL)
        return NULL;
    r = PyObject_GetItem(o, okey);
    Py_DECREF(okey);
    return r;
}

被委托的 PyObject_GetItem(同文件 L154 起)遵循"先 mapping 槽、后 sequence 槽"的查找顺序:若 tp_as_mapping->mp_subscript 存在则直接调用;否则对整数键退回 sq_item 序列语义;两者皆无则报 '%s' object is not subscriptable。写入侧的 PyMapping_SetItemString(L2342-L2359)与删除侧的 PyObject_DelItem(L269-L301)同理,均通过 mp_ass_subscript 落地,删除时以第三个参数 NULL 区分于赋值。

注意 PyMapping_DelItemInclude/abstract.h 中实际上被直接 #definePyObject_DelItem(L830),PyMapping_DelItemString 同理(L820),所以它只是别名,不产生额外行为。

3.2 PyMapping_GetOptionalItem:三态返回 + dict 快速路径

Python 3.13 引入的 PyMapping_GetOptionalItem(obj, key, result)(同文件 L208-L225)解决了 C 扩展里最常见的痛点——if key in d 式探测原本要写"调用 + 检查 KeyError + 清错"三行样板代码。其契约(与 Include/abstract.h L878-L891 注释一致)为:

  • 找到键:返回 1*result 指向值的新强引用(调用方负责 Py_DECREF);
  • 未找到:返回 0*result = NULLKeyError 被静默清除;
  • 其他异常(如 TypeError):返回 -1*result = NULL,异常保留待传播。

源码中有一个值得注意的实现细节——对字典做了精确类型快速路径:

if (PyAnyDict_CheckExact(obj)) {
    return PyDict_GetItemRef(obj, key, result);
}
*result = PyObject_GetItem(obj, key);
if (*result) return 1;
if (!PyErr_ExceptionMatches(PyExc_KeyError)) return -1;
PyErr_Clear();
return 0;

从源码结构看,dict 对象直接走 PyDict_GetItemRef 避免了"先抛异常再捕获"的开销,而通用对象路径则通过 PyErr_ExceptionMatches(PyExc_KeyError) 精确区分"键不存在"与"真正出错",保证三态语义不出错。字符串版 PyMapping_GetOptionalItemString(L2325-L2340)在 key == NULL 时会置 *result = NULL 并报 null_error,同样遵循"造键→委托→释放"模式。

四、键存在性探测:吞异常版与带错误版的选择

key in o 在 C 层有两个家族,差别全在异常策略:

静默版(既有 API):PyMapping_HasKey / PyMapping_HasKeyString "always succeeds"。实现(Objects/abstract.c L2404-L2427、L2379-L2402)内部复用 PyMapping_GetOptionalItem,一旦返回负值就调用 PyErr_FormatUnraisable 打印"Exception ignored in PyMapping_HasKey()..."并对外返回 0;对 NULL 参数为向后兼容做防护而非崩溃。文档明确警告:调用 __getitem__ 过程中发生的异常会被静默忽略,需要正确错误处理时应改用 PyMapping_HasKeyWithErrorPyMapping_GetOptionalItemPyObject_GetItem

带错误版(3.13 新增):PyMapping_HasKeyWithError / PyMapping_HasKeyStringWithError 的实现极为精炼(L2361-L2377)——直接调用 Optional 变体并 Py_XDECREF 丢弃值,透传 1 / 0 / -1 三态,让调用方自行决定如何处理异常:

int
PyMapping_HasKeyWithError(PyObject *obj, PyObject *key)
{
    PyObject *res;
    int rc = PyMapping_GetOptionalItem(obj, key, &res);
    Py_XDECREF(res);
    return rc;
}

选型建议:在 C 扩展中做键探测时,优先使用带错误版,避免像静默版那样把真实的 TypeError 也一并吞掉。

五、大小与遍历:PyMapping_SizeKeys/Values/Items

PyMapping_Size(o) 等价于 len(o)Objects/abstract.c L2275-L2297)。它优先调用 tp_as_mapping->mp_length 槽;若类型只有 sequence 的 sq_length 而无 mapping 槽,报"'...is not a mapping'"的 TypeError。源码注释(L2294)指出 PyMapping_Size() 也可以由 PyObject_Size() 回调,即两者互为支撑。PyMapping_LengthInclude/abstract.h L806-L809 中为 DLL 兼容保留了独立的 PyAPI_FUNC 符号,同时 #define PyMapping_Length PyMapping_Size 让普通构建直接复用同一实现。

PyMapping_Keys/Items/Values 自 Python 3.7 起恒定返回列表(此前可能返回 list 或 tuple,versionchanged 3.7 即针对此变更)。三者的实现(L2459-L2493)共享同一策略:

  1. dict 快速路径PyAnyDict_CheckExact 命中时直接返回 PyDict_Keys/PyDict_Items/PyDict_Values 的结果;
  2. 通用路径:经辅助函数 method_output_as_list(L2432-L2457)调用对象自身的 keys()/items()/values() 方法;若方法输出恰是 list 则原样返回,否则要求其可迭代并用 PySequence_List 归一化为列表,非可迭代输出会报 "...(). must return an iterable, not ..."TypeError

注意三个函数失败时均返回 NULL(而非空列表),且成功返回的是新引用,调用方必须释放。

六、可运行示例:在 C 扩展中使用 Mapping API

以下示例演示一个最小的 C 扩展函数 probe(mapping, name):探测 mapping[name] 是否存在,存在则返回其 repr,否则返回 None,同时展示正确的引用计数管理:

#include <Python.h>

/* 探测 mapping[name],存在则返回其 repr(),不存在返回 None(新引用) */
static PyObject *
probe(PyObject *module, PyObject *mapping, PyObject *name)
{
    PyObject *value = NULL;

    /* 前置检查:对象是否提供 mapping 协议(永不失败) */
    if (PyMapping_Check(mapping) == 0) {
        PyErr_Format(PyExc_TypeError,
                     "%R does not support item access", mapping);
        return NULL;
    }

    /* 3.13+:三态探测,KeyError 被静默处理 */
    int rc = PyMapping_GetOptionalItem(mapping, name, &value);
    if (rc < 0) {
        return NULL;                /* 真实错误,保留异常 */
    }
    if (rc == 0) {
        Py_RETURN_NONE;             /* 键不存在 */
    }

    PyObject *repr = PyObject_Repr(value);
    Py_DECREF(value);                /* 释放 GetOptionalItem 给出的强引用 */
    return repr;
}

static PyMethodDef methods[] = {
    {"probe", (PyCFunction)probe, METH_VARARGS,
     "probe(mapping, name): repr of mapping[name] or None"},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef mod = {
    PyModuleDef_HEAD_INIT, "mapdemo", NULL, -1, methods
};

PyMODINIT_FUNC PyInit_mapdemo(void)
{
    return PyModule_Create(&mod);
}

使用要点:

  • PyMapping_GetOptionalItem 返回 1 时,value新强引用,本例在 Repr 之后必须 Py_DECREF
  • 若目标 Python 低于 3.13,可退化为"调用 PyMapping_GetItemString + PyErr_ExceptionMatches(PyExc_KeyError) + PyErr_Clear()"的等价手写逻辑,或直接用 PyMapping_HasKeyString(注意它会静默吞掉所有异常);
  • 需要键的字符串常量时用 *String 变体;需要保留真实异常(例如 TypeError)时务必选 3.13 的 WithError/Optional 变体。

七、版本与 Limited API 可用性

Include/abstract.h L888-L891 中,PyMapping_GetOptionalItemPyMapping_GetOptionalItemString 被版本门控保护:

#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
PyAPI_FUNC(int) PyMapping_GetOptionalItem(PyObject *, PyObject *, PyObject **);
PyAPI_FUNC(int) PyMapping_GetOptionalItemString(PyObject *, const char *, PyObject **);
#endif

这意味着这两个函数在 Stable ABI(Py_LIMITED_API)下要求目标解释器为 3.13(0x030d0000)及以上;而 PyMapping_HasKeyWithError / PyMapping_HasKeyStringWithError 同样标注 versionadded 3.13。如果你的扩展声明了较低版本门限的 limited API,引用这些新符号会直接编译失败,这是使用 3.13 新 API 时必须核对的兼容前提。仓库中的 Lib/test/test_stable_abi_ctypes.py 会对这些新符号的 Stable ABI 导出进行 ctypes 级验证,可作为"符号是否真实导出"的测试证据。

八、小结与源码索引

PyMapping_* 函数族围绕"类型槽 → 便捷封装 → 三态错误策略"三层组织:底层 PyObject_GetItem/SetItem/DelItemtp_as_mappingmp_subscript / mp_ass_subscript / mp_length 槽与对象交互;*String 变体加一层 UTF-8 键转换;3.13 的 OptionalWithError 变体则统一了"键探测"的错误语义,并内置了 dict 快速路径。写 C 扩展时的推荐路径是:存在性探测用 PyMapping_GetOptionalItem(或 *WithError),取值失败需区分 KeyError 与真实错误、遍历用 PyMapping_Items、键为字面量时用 *String 变体,同时严格遵守"成功返回值/1 结果均为新引用"的释放义务。

本文涉及的源码与文档路径:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384