CPython 数值协议深度解析:PyNumber_* API 族与 tp_as_number 槽位机制
本篇技术指南以 CPython 官方文档 number.rst 描述的 Number Protocol(数值协议) 为主体,逐一讲解 PyNumber_* 系列 C API 的功能、等价 Python 表达式与参数约定,并结合 Objects/abstract.c、Include/cpython/object.h 等源码,深入剖析二元运算符的分派与反射机制、原地操作的回退规则、以及 PyNumber_Index / PyNumber_AsSsize_t / PyNumber_ToBase 等转换函数的底层实现细节。读完后,你能够在编写 C 扩展时安全地使用这些 API 完成数值运算、类型判断与整数转换,并理解 CPython 解释器在 +、+=、pow() 等语法背后真正调用了哪些函数。
什么是数值协议:tp_as_number 与 PyNumberMethods
数值协议是 CPython 抽象层(abstract API)中负责处理数值对象的一组函数,其核心思想是:不关心对象的具体类型,而是查询类型对象中注册的槽位函数(slot)来完成运算。
在 C 层面,每个类型通过 PyTypeObject 的 tp_as_number 成员挂载一个 PyNumberMethods 结构体(见 object.h 的 _typeobject 定义)。该结构体的完整槽位定义位于 Include/cpython/object.h:
typedef struct {
binaryfunc nb_add; /* 对应 + */
binaryfunc nb_subtract; /* 对应 - */
binaryfunc nb_multiply; /* 对应 * */
binaryfunc nb_remainder; /* 对应 % */
binaryfunc nb_divmod; /* 对应 divmod() */
ternaryfunc nb_power; /* 对应 ** 与 pow() */
unaryfunc nb_negative; /* 对应 -o */
unaryfunc nb_positive; /* 对应 +o */
unaryfunc nb_absolute; /* 对应 abs() */
inquiry nb_bool; /* 对应 bool() */
unaryfunc nb_invert; /* 对应 ~ */
binaryfunc nb_lshift; /* 对应 << */
binaryfunc nb_rshift; /* 对应 >> */
binaryfunc nb_and; /* 对应 & */
binaryfunc nb_xor; /* 对应 ^ */
binaryfunc nb_or; /* 对应 | */
unaryfunc nb_int; /* 对应 int() 转换 */
void *nb_reserved; /* 原 nb_long 槽位 */
unaryfunc nb_float; /* 对应 float() 转换 */
binaryfunc nb_inplace_add; /* += */
/* ... 其余 inplace 槽位略 ... */
binaryfunc nb_floor_divide; /* // */
binaryfunc nb_true_divide; /* / */
binaryfunc nb_inplace_floor_divide;
binaryfunc nb_inplace_true_divide;
unaryfunc nb_index; /* __index__ */
binaryfunc nb_matrix_multiply; /* @,3.5 引入 */
binaryfunc nb_inplace_matrix_multiply;
} PyNumberMethods;
需要注意结构体开头的注释约束:槽位实现必须自行检查两个参数的类型并做必要的转换,不能依赖调用方做类型强制。
所有 PyNumber_* 函数均在 Include/abstract.h 中声明(如 PyNumber_Add 声明、PyNumber_Index 声明、PyNumber_AsSsize_t 声明、PyNumber_ToBase 声明),实现集中在 Objects/abstract.c。
类型判断:PyNumber_Check 与 PyIndex_Check
PyNumber_Check
PyNumber_Check(PyObject *o) 在对象 o 提供数值协议时返回 1,否则返回 0。该函数总是成功,不会设置异常。自 Python 3.8 起,"索引整数"(实现了 __index__)也视为数值对象。
从源码看,其判定条件非常简洁(abstract.c 中的 PyNumber_Check):
int
PyNumber_Check(PyObject *o)
{
if (o == NULL)
return 0;
PyNumberMethods *nb = Py_TYPE(o)->tp_as_number;
return nb && (nb->nb_index || nb->nb_int || nb->nb_float || PyComplex_Check(o));
}
即只要 tp_as_number 非空且 nb_index、nb_int、nb_float 任一槽位被填充,或对象是复数,就被认定为数值对象。
PyIndex_Check
PyIndex_Check(PyObject *o) 当 o 是"索引整数"时返回 1,否则返回 0。所谓索引整数,是指 tp_as_number 结构中的 nb_index 槽位(对应 Python 的 __index__ 方法)已被填充。该函数总是成功,是 C 扩展中验证"这个对象能不能当序列下标/循环计数"的常用工具。实现位于 abstract.c 中的 PyIndex_Check,内部委托给 _PyIndex_Check。
二元运算符 API 族
以下函数全部遵循统一约定:成功时返回新创建的、持有新引用的 PyObject * 结果,失败时返回 NULL 并设置 Python 异常(调用方需检查返回值)。
| C API | 等价 Python 表达式 | 说明 |
|---|---|---|
PyNumber_Add(o1, o2) |
o1 + o2 |
相加;左侧类型不支持时回退序列拼接 |
PyNumber_Subtract(o1, o2) |
o1 - o2 |
从 o1 中减去 o2 |
PyNumber_Multiply(o1, o2) |
o1 * o2 |
相乘;回退序列重复 |
PyNumber_MatrixMultiply(o1, o2) |
o1 @ o2 |
矩阵乘法(3.5 起) |
PyNumber_FloorDivide(o1, o2) |
o1 // o2 |
取地板除 |
PyNumber_TrueDivide(o1, o2) |
o1 / o2 |
真除法 |
PyNumber_Remainder(o1, o2) |
o1 % o2 |
取模 |
PyNumber_Divmod(o1, o2) |
divmod(o1, o2) |
同时返回商与余元组 |
PyNumber_Power(o1, o2, o3) |
pow(o1, o2, o3) |
幂运算,o3 可选 |
PyNumber_Lshift(o1, o2) |
o1 << o2 |
左移 |
PyNumber_Rshift(o1, o2) |
o1 >> o2 |
右移 |
PyNumber_And(o1, o2) |
o1 & o2 |
按位与 |
PyNumber_Xor(o1, o2) |
o1 ^ o2 |
按位异或 |
PyNumber_Or(o1, o2) |
o1 | o2 |
按位或 |
值得注意的几个语义细节
真除法的结果类型。 PyNumber_TrueDivide 文档明确说明:返回值是对数学商的"合理近似",因为二进制浮点无法精确表示所有实数;传入两个整数时也可能返回浮点值(等价于 Python 3 中 1 / 2 得到 0.5)。
幂运算的三参数形式。 PyNumber_Power 等价于 pow(o1, o2, o3),o3 是可选的模数参数。文档特别警告:如果不需要模数,必须传 Py_None,绝不能传 NULL,否则会引发非法内存访问。源码中 _PyNumber_PowerNoMod 正是这样调用它的(abstract.c):
PyObject *
_PyNumber_PowerNoMod(PyObject *lhs, PyObject *rhs)
{
return PyNumber_Power(lhs, rhs, Py_None);
}
分派机制:谁先尝试运算?
理解 PyNumber_Add 等函数的关键,是 Objects/abstract.c 中 binary_op1 实现的调用顺序(源码注释原文为 "Order operations are tried until either a valid result or error"):
w.op(v, w) [*], v.op(v, w), w.op(v, w)
[*] 仅当 Py_TYPE(v) != Py_TYPE(w) 且 w 的类型是 v 类型的子类时
即:
- 若右操作数 w 的类型是左操作数 v 类型的真子类且两者槽位不同,先尝试右操作数的反射实现(对应 Python 中
__radd__优先于__add__的规则),若返回Py_NotImplemented则弃用; - 再尝试左操作数的实现;
- 最后尝试右操作数的普通实现;
- 三方都返回
Py_NotImplemented时,函数最终返回它并交由外层处理。
以 PyNumber_Add 为例(abstract.c):
PyObject *
PyNumber_Add(PyObject *v, PyObject *w)
{
PyObject *result = BINARY_OP1(v, w, NB_SLOT(nb_add), "+");
if (result != Py_NotImplemented) {
return result;
}
Py_DECREF(result);
PySequenceMethods *m = Py_TYPE(v)->tp_as_sequence;
if (m && m->sq_concat) {
result = (*m->sq_concat)(v, w);
...
return result;
}
return binop_type_error(v, w, "+");
}
可见 + 在数值槽位都不支持时还有一个序列回退:调用左操作数的 sq_concat(__concat__),这使得 'ab' + 'cd'、[1] + [2] 这类拼接能走 PyNumber_Add 的入口而最终给出 TypeError 或拼接结果。PyNumber_Multiply 同理,回退到 sq_repeat(__repeat__),并强制右操作数是索引整数,否则报 can't multiply sequence by non-int 类型的 TypeError(abstract.c)。
其余大多数二元操作数是通过宏批量生成的,例如(abstract.c):
#define BINARY_FUNC(func, op, op_name) \
PyObject * \
func(PyObject *v, PyObject *w) { \
return binary_op(v, w, NB_SLOT(op), op_name); \
}
BINARY_FUNC(PyNumber_Or, nb_or, "|")
BINARY_FUNC(PyNumber_Xor, nb_xor, "^")
BINARY_FUNC(PyNumber_And, nb_and, "&")
BINARY_FUNC(PyNumber_Lshift, nb_lshift, "<<")
BINARY_FUNC(PyNumber_Rshift, nb_rshift, ">>")
BINARY_FUNC(PyNumber_Subtract, nb_subtract, "-")
BINARY_FUNC(PyNumber_Divmod, nb_divmod, "divmod()")
一元运算符 API 族
| C API | 等价 Python 表达式 | 说明 |
|---|---|---|
PyNumber_Negative(o) |
-o |
取负 |
PyNumber_Positive(o) |
+o |
取正(通常等价于返回自身) |
PyNumber_Absolute(o) |
abs(o) |
绝对值 |
PyNumber_Invert(o) |
~o |
按位取反 |
四个函数签名统一为单参数 PyObject *(PyObject *),失败时返回 NULL。它们映射到 PyNumberMethods 中的 nb_negative、nb_positive、nb_absolute、nb_invert 四个 unaryfunc 槽位。
原地运算符:PyNumber_InPlace* 系列
每一个二元运算都有对应的原地版本,共 15 个:
PyNumber_InPlaceAdd、PyNumber_InPlaceSubtract、PyNumber_InPlaceMultiply、PyNumber_InPlaceMatrixMultiply(3.5 起)、PyNumber_InPlaceFloorDivide、PyNumber_InPlaceTrueDivide、PyNumber_InPlaceRemainder、PyNumber_InPlacePower、PyNumber_InPlaceLshift、PyNumber_InPlaceRshift、PyNumber_InPlaceAnd、PyNumber_InPlaceXor、PyNumber_InPlaceOr,分别等价于 o1 += o2、o1 -= o2、o1 *= o2、o1 @= o2、o1 //= o2、o1 /= o2、o1 %= o2、o1 **= o2、o1 <<= o2、o1 >>= o2、o1 &= o2、o1 ^= o2、o1 |= o2。
其中 PyNumber_InPlacePower(o1, o2, o3) 的三参数约定与普通版本相同:忽略模数时传 Py_None 而非 NULL;o3 为 Py_None 时等价于 o1 **= o2,否则是 pow(o1, o2, o3) 的原地变体。
abstract.c 中有一段关键注释 说明了原地操作的语义规则:
/* Binary in-place operators */
/* The in-place operators are defined to fall back to the 'normal',
non in-place operations, if the in-place methods are not in place.
- If the left hand object has the appropriate struct members, and
they are filled, call the appropriate function and return the
result. No coercion is done on the arguments; the left-hand object
is the one the operation is performed on, and it's up to the
function to deal with the right-hand object.
- Otherwise, in-place modification is not supported. Handle it exactly as
a non in-place operation of the same kind.
*/
也就是说:如果左操作数的 nb_inplace_* 槽位已填充,就调用它;否则完全按普通(非原地)运算处理。这与 Python 层面"没有 __iadd__ 时回退到 __add__"的行为一致。实现函数 binary_iop1 位于 abstract.c。注意原地操作不做参数类型强制,对右操作数的处理责任完全在槽位函数自身。
数值转换函数:Long、Float、Index、ToBase、AsSsize_t
PyNumber_Long 与 PyNumber_Float
PyNumber_Long(o):等价于int(o),将 o 转换为整数对象;PyNumber_Float(o):等价于float(o),将 o 转换为浮点对象。
两者失败时返回 NULL 并设置异常。
PyNumber_Index:保证精确 int
PyNumber_Index(o) 将 o 转换为 Python int,失败时抛出 TypeError 并返回 NULL。自 Python 3.10 起,结果类型恒为精确的 int(之前可能返回 int 的子类实例)。
从源码看,公开 API 是在内部函数 _PyNumber_Index 之上加了一层"精确类型化"(abstract.c):
PyObject *
PyNumber_Index(PyObject *item)
{
PyObject *result = _PyNumber_Index(item);
if (result != NULL && !PyLong_CheckExact(result)) {
Py_SETREF(result, _PyLong_Copy((PyLongObject *)result));
}
return result;
}
内部函数 _PyNumber_Index(abstract.c)的完整逻辑为:
item为NULL时设置SystemError;- 若
PyLong_Check(item)(本身是 int 或 int 子类),直接返回新引用; - 若
!_PyIndex_Check(item)(没有__index__),抛出TypeError: '<type>' object cannot be interpreted as an integer; - 调用
nb_index槽位;结果不是 int 时抛TypeError: __index__() must return an int; - 结果是 int 的严格子类时发出
DeprecationWarning(对应 issue #17576)。
PyNumber_ToBase:进制转换字符串
PyNumber_ToBase(PyObject *n, int base) 将整数 n 按 base 进制转换为字符串,base 只允许 2、8、10、16;base 为 2/8/16 时返回的字符串带 '0b'/'0o'/'0x' 前缀。若 n 不是 Python int,先经 PyNumber_Index 转换。
实现非常短,同时揭示了非法 base 的行为(abstract.c):
PyObject *
PyNumber_ToBase(PyObject *n, int base)
{
if (!(base == 2 || base == 8 || base == 10 || base == 16)) {
PyErr_SetString(PyExc_SystemError,
"PyNumber_ToBase: base must be 2, 8, 10 or 16");
return NULL;
}
PyObject *index = _PyNumber_Index(n);
if (!index)
return NULL;
PyObject *res = _PyLong_Format(index, base);
Py_DECREF(index);
return res;
}
注意 base 非法时设置的是 SystemError(这是 C API 调用方传参错误的信号,而非 Python 层的 ValueError),随后通过 _PyLong_Format 完成格式化。
PyNumber_AsSsize_t:到 C 整数的受控收窄
PyNumber_AsSsize_t(PyObject *o, PyObject *exc) 将 o 解释为整数并收窄为 Py_ssize_t,失败时返回 -1 并设置异常。第二个参数 exc 控制溢出时的行为,这是很多扩展开发者容易踩坑的地方:
exc非 NULL(通常传PyExc_IndexError或PyExc_OverflowError):若 o 能转换为 Python int 但超出Py_ssize_t范围,抛出 exc 指定的异常类型(异常消息为cannot fit '<type>' into an index-sized integer);exc为 NULL:溢出异常被清除,值被裁剪为PY_SSIZE_T_MIN(负数)或PY_SSIZE_T_MAX(正数),函数返回裁剪后的值。
实现逻辑位于 abstract.c 中的 PyNumber_AsSsize_t:先经 _PyNumber_Index 得到精确 int,再调 PyLong_AsSsize_t;仅当捕获到 OverflowError 时才进入上述两条分支,其它异常原样向上传播。CPython 内部大量使用它,例如 PySequence_GetItem 处理负索引、PyNumber_Multiply 的序列重复等。
在 C 扩展中使用数值协议的实用约定
结合文档与源码,编写调用 PyNumber_* 的扩展代码时应遵循:
/* 加法示例:注意结果必须检查 NULL 并管理引用计数 */
PyObject *sum = PyNumber_Add(a, b);
if (sum == NULL) {
return NULL; /* 异常已由 API 设置,直接传播 */
}
Py_DECREF(a);
Py_DECREF(b);
/* 使用 sum ... */
Py_DECREF(sum);
/* 需要 "当作下标" 的整数时 */
if (!PyIndex_Check(n)) {
PyErr_SetString(PyExc_TypeError, "index must be an integer");
return NULL;
}
Py_ssize_t idx = PyNumber_AsSsize_t(n, PyExc_IndexError);
if (idx == -1 && PyErr_Occurred()) {
return NULL;
}
/* 幂运算不需要模数时传 Py_None,绝不能传 NULL */
PyObject *p = PyNumber_Power(base, exp, Py_None);
要点归纳:
- 返回值即新引用:所有
PyNumber_*成功返回值都持有新引用,用完必须Py_DECREF; NULL即失败:失败时异常已被设置,不要重复PyErr_Set*;- 三参数幂运算的第三参传
Py_None,NULL会导致非法内存访问; - 判断"能否作下标"用
PyIndex_Check+PyNumber_AsSsize_t,并区分-1与是否有异常(PyErr_Occurred()); PyNumber_TrueDivide可能返回 float,不要假定两个整数相除仍是整数。
相关源码与文档索引
| 内容 | 路径 |
|---|---|
| 数值协议文档(本文主体) | Doc/c-api/number.rst |
| API 声明 | Include/abstract.h |
| 核心实现(分派、回退、转换) | Objects/abstract.c |
PyNumberMethods 结构定义 |
Include/cpython/object.h |
tp_as_number 挂载点 |
Include/cpython/object.h |
槽位编号常量(如 Py_nb_add) |
Include/slots_generated.h |
小结
Doc/c-api/number.rst 定义的 PyNumber_* API 族是 CPython 数值协议的完整 C 层接口:二元/一元/按位运算各自映射到 PyNumberMethods 的对应槽位,原地运算在未实现时透明回退为普通运算,PyNumber_Index、PyNumber_AsSsize_t、PyNumber_ToBase 则覆盖从"对象"到"精确 int / C 整数 / 进制字符串"的三条转换路径。深入 Objects/abstract.c 的 binary_op1 分派顺序与 PyNumber_Add/PyNumber_Multiply 的序列回退逻辑,可以看到 C API 的每个函数都不是简单的函数指针转发,而是精确复刻了 Python 语法层面的运算符重载、反射优先与序列兼容语义。
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 StartedRust0623
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