首页
/ CPython 数值协议深度解析:PyNumber_* API 族与 tp_as_number 槽位机制

CPython 数值协议深度解析:PyNumber_* API 族与 tp_as_number 槽位机制

2026-09-04 22:32:50作者:丁柯新Fawn

本篇技术指南以 CPython 官方文档 number.rst 描述的 Number Protocol(数值协议) 为主体,逐一讲解 PyNumber_* 系列 C API 的功能、等价 Python 表达式与参数约定,并结合 Objects/abstract.cInclude/cpython/object.h 等源码,深入剖析二元运算符的分派与反射机制、原地操作的回退规则、以及 PyNumber_Index / PyNumber_AsSsize_t / PyNumber_ToBase 等转换函数的底层实现细节。读完后,你能够在编写 C 扩展时安全地使用这些 API 完成数值运算、类型判断与整数转换,并理解 CPython 解释器在 ++=pow() 等语法背后真正调用了哪些函数。

什么是数值协议:tp_as_number 与 PyNumberMethods

数值协议是 CPython 抽象层(abstract API)中负责处理数值对象的一组函数,其核心思想是:不关心对象的具体类型,而是查询类型对象中注册的槽位函数(slot)来完成运算

在 C 层面,每个类型通过 PyTypeObjecttp_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_indexnb_intnb_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.cbinary_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 类型的子类时

即:

  1. 若右操作数 w 的类型是左操作数 v 类型的真子类且两者槽位不同,先尝试右操作数的反射实现(对应 Python 中 __radd__ 优先于 __add__ 的规则),若返回 Py_NotImplemented 则弃用;
  2. 再尝试左操作数的实现;
  3. 最后尝试右操作数的普通实现;
  4. 三方都返回 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 类型的 TypeErrorabstract.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_negativenb_positivenb_absolutenb_invert 四个 unaryfunc 槽位。

原地运算符:PyNumber_InPlace* 系列

每一个二元运算都有对应的原地版本,共 15 个:

PyNumber_InPlaceAddPyNumber_InPlaceSubtractPyNumber_InPlaceMultiplyPyNumber_InPlaceMatrixMultiply(3.5 起)、PyNumber_InPlaceFloorDividePyNumber_InPlaceTrueDividePyNumber_InPlaceRemainderPyNumber_InPlacePowerPyNumber_InPlaceLshiftPyNumber_InPlaceRshiftPyNumber_InPlaceAndPyNumber_InPlaceXorPyNumber_InPlaceOr,分别等价于 o1 += o2o1 -= o2o1 *= o2o1 @= o2o1 //= o2o1 /= o2o1 %= o2o1 **= o2o1 <<= o2o1 >>= o2o1 &= o2o1 ^= o2o1 |= o2

其中 PyNumber_InPlacePower(o1, o2, o3) 的三参数约定与普通版本相同:忽略模数时传 Py_None 而非 NULLo3Py_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_Indexabstract.c)的完整逻辑为:

  1. itemNULL 时设置 SystemError
  2. PyLong_Check(item)(本身是 int 或 int 子类),直接返回新引用;
  3. !_PyIndex_Check(item)(没有 __index__),抛出 TypeError: '<type>' object cannot be interpreted as an integer
  4. 调用 nb_index 槽位;结果不是 int 时抛 TypeError: __index__() must return an int
  5. 结果是 int 的严格子类时发出 DeprecationWarning(对应 issue #17576)。

PyNumber_ToBase:进制转换字符串

PyNumber_ToBase(PyObject *n, int base) 将整数 nbase 进制转换为字符串,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_IndexErrorPyExc_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);

要点归纳:

  1. 返回值即新引用:所有 PyNumber_* 成功返回值都持有新引用,用完必须 Py_DECREF
  2. NULL 即失败:失败时异常已被设置,不要重复 PyErr_Set*
  3. 三参数幂运算的第三参传 Py_NoneNULL 会导致非法内存访问;
  4. 判断"能否作下标"用 PyIndex_Check + PyNumber_AsSsize_t,并区分 -1 与是否有异常(PyErr_Occurred());
  5. 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_IndexPyNumber_AsSsize_tPyNumber_ToBase 则覆盖从"对象"到"精确 int / C 整数 / 进制字符串"的三条转换路径。深入 Objects/abstract.cbinary_op1 分派顺序与 PyNumber_Add/PyNumber_Multiply 的序列回退逻辑,可以看到 C API 的每个函数都不是简单的函数指针转发,而是精确复刻了 Python 语法层面的运算符重载、反射优先与序列兼容语义。

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

项目优选

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