CPython C API Mapping 协议详解:PyMapping_* 函数族完整参考与源码级实现剖析
本文以 CPython 官方文档 Mapping Protocol 为核心,系统讲解 C API 中全部 12 个 PyMapping_* 函数的语义、返回值约定与版本演进,并结合 Objects/abstract.c、Include/abstract.h 的源码实现,说明这些函数如何通过类型槽 tp_as_mapping 与 PyMappingMethods 结构落地,帮助你在 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 _typeobject 的 tp_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) |
找不到键不抛 KeyError 的 PyObject_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_DelItem 在 Include/abstract.h 中实际上被直接 #define 为 PyObject_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 = NULL,KeyError被静默清除; - 其他异常(如
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_HasKeyWithError、PyMapping_GetOptionalItem 或 PyObject_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_Size 与 Keys/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_Length 在 Include/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)共享同一策略:
- dict 快速路径:
PyAnyDict_CheckExact命中时直接返回PyDict_Keys/PyDict_Items/PyDict_Values的结果; - 通用路径:经辅助函数
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_GetOptionalItem 与 PyMapping_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/DelItem 经 tp_as_mapping 的 mp_subscript / mp_ass_subscript / mp_length 槽与对象交互;*String 变体加一层 UTF-8 键转换;3.13 的 Optional 与 WithError 变体则统一了"键探测"的错误语义,并内置了 dict 快速路径。写 C 扩展时的推荐路径是:存在性探测用 PyMapping_GetOptionalItem(或 *WithError),取值失败需区分 KeyError 与真实错误、遍历用 PyMapping_Items、键为字面量时用 *String 变体,同时严格遵守"成功返回值/1 结果均为新引用"的释放义务。
本文涉及的源码与文档路径:
- 协议文档:Doc/c-api/mapping.rst
- 核心实现:Objects/abstract.c(
PyMapping_GetOptionalItemL208-L225、PyMapping_CheckL2268-L2273、PyMapping_SizeL2275-L2297、HasKey*家族 L2361-L2427、Keys/Items/ValuesL2432-L2493) - 公开声明:Include/abstract.h(L795-L898,含
PyMapping_DelItem的#define别名与 Limited API 门控) - 类型槽定义:Include/cpython/object.h(
PyMappingMethodsL122-L126、tp_as_mappingL167) - Stable ABI 符号验证:Lib/test/test_stable_abi_ctypes.py
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00