CPython 整数对象 C API 完全指南:PyLong 转换、导出布局与 PyLongWriter 深入解析
在 CPython C 扩展开发中,Python 的 int 是唯一"无限精度"的数值类型,也是与 C 数值类型交互最频繁的桥梁:几乎所有接收 Py_ssize_t、long、size_t 参数的 C API 都需要先做一次 PyLong 转换。本文基于 CPython 官方文档 Doc/c-api/long.rst 与核心实现 Objects/longobject.c、Include/longobject.h、Include/cpython/longobject.h,系统梳理整数对象("long" integer objects)的完整 C API 面:类型检查、构造与转换函数、-1 错误约定的辨析、3.13 引入的原生字节转换、3.14 新增的定宽整数 API、紧凑整数快速路径,以及 3.14 全新的整数导出/导入 API(PyLong_Export 与 PyLongWriter)。读完本文,你将能够正确、安全地在 C 扩展中与 Python 大整数互转,并编写不依赖 CPython 内部布局细节的可移植代码。
一、核心概念:所有整数都是 long 对象
CPython 文档开篇即给出一个关键事实:所有 Python 整数都实现为任意大小的 "long" 整数对象。从 Python 3.0 开始,PyIntObject 已被移除,PyLongObject 是唯一承载 int 的类型,因此 len(x)、range() 上下界、文件偏移等场景一律走 PyLong API。
文档同时给出了使用 PyLong 转换函数时最重要的错误约定:
出错时,大多数
PyLong_As*API 返回(返回类型)-1,而这无法与一个合法数值区分。请使用PyErr_Occurred来消歧。
也就是说,PyLong_AsLong 返回 -1 既可能是"值就是 -1",也可能是"发生了 OverflowError"。判断方式固定为:
long value = PyLong_AsLong(obj);
if (value == -1 && PyErr_Occurred()) {
// 出错:处理或传播异常
return NULL;
}
无符号版本同理(返回 (unsigned long)-1、(size_t)-1 等),PyLong_AsDouble 出错时返回 -1.0,PyLong_AsVoidPtr 出错时返回 NULL,全部依赖 PyErr_Occurred 消歧。
1.1 PyLongObject 与 PyLong_Type
PyLongObject:PyObject的子类型,表示一个 Python 整数对象。其完整结构体定义属于 CPython 内部头文件,外部扩展应通过PyLong_Check检查后以PyLongObject*方式使用;PyLong_Type:表示 Python 整数类型的PyTypeObject实例,与 Python 层的int是同一个对象;PyLong_Check(op)/PyLong_CheckExact(op):前者在op是PyLongObject或其子类型时返回真,后者仅接受精确的int类型(不接受子类)。从 Include/longobject.h 可以看到两者的宏实现——PyLong_Check通过PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LONG_SUBCLASS)检查子类标记位,因此永远成功、开销极低,这也是文档强调"This function always succeeds"的原因。
二、从 C 值构造 Python 整数(PyLong_From* 家族)
构造函数统一约定"返回新的 PyLongObject,失败返回 NULL",覆盖 C 语言各种定宽与不定宽整型:
| 函数 | 来源类型 | 说明 |
|---|---|---|
PyLong_FromLong(long) |
long |
最基础的构造器 |
PyLong_FromUnsignedLong(unsigned long) |
unsigned long |
无符号版本 |
PyLong_FromSsize_t(Py_ssize_t) |
Py_ssize_t |
用于序列长度、下标等 |
PyLong_FromSize_t(size_t) |
size_t |
用于大小/偏移量 |
PyLong_FromLongLong(long long) |
long long |
64 位有符号场景 |
PyLong_FromUnsignedLongLong(unsigned long long) |
unsigned long long |
64 位无符号场景 |
PyLong_FromInt32(int32_t) / PyLong_FromInt64(int64_t) |
int32_t/int64_t |
3.14 新增,失败时设置异常 |
PyLong_FromUInt32(uint32_t) / PyLong_FromUInt64(uint64_t) |
uint32_t/uint64_t |
3.14 新增 |
PyLong_FromDouble(double) |
double |
取 v 的整数部分 |
PyLong_FromString(const char *str, char **pend, int base) |
字符串 | 按进制解析,见下文 |
PyLong_FromUnicodeObject(PyObject *u, int base) |
Unicode 串 | 3.3 添加 |
PyLong_FromVoidPtr(void *p) |
指针 | 与 PyLong_AsVoidPtr 成对使用 |
PyLong_FromNativeBytes(...) / PyLong_FromUnsignedNativeBytes(...) |
字节缓冲 | 3.13 添加,见第五节 |
PyLong_FromPid(pid) |
pid_t |
宏,按系统 PID 类型别名到 FromLong 或 FromLongLong,3.2 添加 |
2.1 小整数缓存(CPython 实现细节)
文档在 PyLong_FromLong 条目下以 impl-detail 明确说明:
CPython 维护一个覆盖
-5到1024所有整数的对象数组。创建该范围内的 int 时,实际得到的是对已有对象的引用。
这就是 Python 中 a = 5; b = 5; a is b 为 True 的底层原因(编译期常量折进字节码后同理)。使用该范围之外的值时不要依赖身份相等,is 只应作用于 is None 这类单例判断。
2.2 PyLong_FromString:进制与下划线规则
PyLong_FromString(str, pend, base) 的解析规则与 Python 层 int() 语义对齐,细节值得逐条掌握:
base == 0时按 Python 整数字面量规则解释(0x、``0o、0b前缀等);此时**非零十进制数的前导零会触发ValueError**(即int("010")` 的报错规则);base != 0时必须介于2和36(含)之间;- 前导/尾随空白被忽略;进制指示符之后的单个下划线、以及数字之间的下划线(如
"1_000_000")同样被忽略; pend非NULL时,成功指向str末尾、失败指向第一个无法处理的字符;- 没有数字,或数字后接空白且未以 NUL 结尾时,抛出
ValueError。
文档还提示:若需要以"底 256 的字节数组"形式做整数转换,应改用 PyLong_AsNativeBytes() / PyLong_FromNativeBytes()。
三、从 Python 整数转 C 值(PyLong_As* 家族)
As 家族函数按目标 C 类型划分,文档按"溢出是否抛异常"分成两类行为:
会抛 OverflowError 的函数(值超出目标类型范围即异常):
PyLong_AsLong(PyObject *obj):转long。注意 obj 不要求是int——若它不是PyLongObject实例,会先调用其__index__方法(若存在)转换为整数;3.8 起开始使用__index__,3.10 起不再回退到__int__;PyLong_AsLongLong(PyObject *obj)、PyLong_AsSsize_t、PyLong_AsSize_t、PyLong_AsUnsignedLong、PyLong_AsUnsignedLongLong(3.1 起负值改为抛OverflowError而非TypeError);PyLong_AsInt(PyObject *obj):3.13 新增,与AsLong类似但结果为int;PyLong_AsDouble(PyObject *pylong):转double,超出double范围抛OverflowError,出错返回-1.0;PyLong_AsVoidPtr(PyObject *pylong):转void*,保证与PyLong_FromVoidPtr往返可用;- 3.14 新增的定宽转换:
PyLong_AsInt32(PyObject *obj, int32_t *value)/PyLong_AsInt64/PyLong_AsUInt32/PyLong_AsUInt64。它们采用"输出参数 + 返回码"风格:成功时写入*value并返回0,出错设置异常并返回-1;*value不允许为NULL;无符号版本遇到负值抛ValueError,超范围抛OverflowError;非int输入同样会先走__index__。
溢出时返回"截断/取模"值而不抛异常的函数:
PyLong_AsUnsignedLongMask(PyObject *obj):超出unsigned long范围时返回该值对ULONG_MAX + 1取模的结果;PyLong_AsUnsignedLongLongMask(PyObject *obj):同理对ULLONG_MAX + 1取模。两者都支持__index__转换(3.8 起),不再使用__int__(3.10 起)。
AndOverflow 变体——不抛异常、用输出参数报告溢出方向:
PyLong_AsLongAndOverflow(PyObject *obj, int *overflow):值大于LONG_MAX时设*overflow = 1并返回-1;小于LONG_MIN时设*overflow = -1并返回-1;无溢出时*overflow = 0。发生其他异常时同样*overflow = 0、返回-1;PyLong_AsLongLongAndOverflow(PyObject *obj, int *overflow):3.2 添加,逻辑相同,边界为LLONG_MAX/LLONG_MIN。
这两者适合实现"钳位到范围"语义而不希望异常打断控制流的场景。
3.1 PyLong_AS_LONG:一个正在被弃用的陷阱
文档单独用 c:namespace 列出了 PyLong_AS_LONG(PyObject *obj) 宏:
与推荐的
PyLong_AsLong完全等价,因此同样可能以OverflowError或其他异常失败。3.14 起软弃用(soft-deprecated)。
从 Include/longobject.h 可见其定义就是 #define PyLong_AS_LONG(op) PyLong_AsLong(op)——名字里的大写 AS 容易让人误以为"绝对成功",实际上它可能失败。如果你在扩展里搜到这个宏,应直接替换为 PyLong_AsLong 并补上 PyErr_Occurred 检查。
3.2 3.14 新增的符号查询 API
除了 PyLong_GetSign(PyObject *obj, int *sign)(成功时 *sign 为 0/-1/+1,返回 0;int 及其子类输入时总是成功),3.14 还提供三个布尔查询:
PyLong_IsPositive(PyObject *obj):obj > 0返回 1,否则 0;PyLong_IsNegative(PyObject *obj):obj < 0返回 1,否则 0;PyLong_IsZero(PyObject *obj):obj == 0返回 1,否则 0。
三者对 int 及其子类永远成功;非整数输入则设置异常并返回 -1。这些函数把"判断符号"从"转成 C 数值再比较"的绕路中解放出来,避免了 -1 歧义问题。
四、PyLong_GetInfo 与紧凑整数快速路径
4.1 PyLong_GetInfo
PyLong_GetInfo(void)(3.1 添加)成功时返回一个只读 named tuple,包含 Python 整数内部表示的信息,各字段含义与 sys.int_info 一致(bits_per_digit、digits_per_word 等)。这是外部代码在不触碰内部头文件的前提下获知"CPython 用什么进制存大数"的正规入口。
4.2 PyUnstable_Long_IsCompact / CompactValue
3.12 添加的一组不稳定 API:
PyUnstable_Long_IsCompact(const PyLongObject* op):op是"紧凑"整数返回 1,否则 0;PyUnstable_Long_CompactValue(const PyLongObject* op):仅当对象紧凑时返回其值,否则返回值未定义。
文档明确定位为性能关键代码的快速路径:先 IsCompact 判定,紧凑值直接用 CompactValue,否则回落到 PyLong_AsSize_t 等 PyLong_As* 函数或 PyLong_AsNativeBytes。文档也坦率提示"对多数用户提速可忽略",且"什么算紧凑是纯实现细节,可能随时变化"——PyUnstable_ 前缀正是这种"不保证 ABI 稳定"的标记。
五、PyLong_AsNativeBytes 与 PyLong_FromNativeBytes:原生字节互转
3.13 引入的一对函数,用于在 Python 整数与"机器字长的原生整数缓冲"之间高效互转(典型场景:把 Python 整数写入 int32_t 变量、从 uint64_t 读出 Python int):
Py_ssize_t PyLong_AsNativeBytes(PyObject *pylong, void *buffer, Py_ssize_t n_bytes, int flags);
PyObject *PyLong_FromNativeBytes(const void *buffer, size_t n_bytes, int flags);
PyObject *PyLong_FromUnsignedNativeBytes(const void *buffer, size_t n_bytes, int flags);
5.1 AsNativeBytes 的返回值语义
返回值是"存储该值所需的字节数",且永远不为 0:
- 返回值
< 0:出错(对象不可解释为整数,或设置了Py_ASNATIVEBYTES_REJECT_NEGATIVE而值为负),已设置异常; - 返回值
<= n_bytes:完整复制成功;缓冲的全部 n_bytes 都会被写入,剩余字节用符号位的拷贝填充; - 返回值
> n_bytes:值被截断——只写入能放进缓冲的低位,高位丢弃。这不是错误,等价于 C 风格的向下强转(downcast),"溢出"不算失败。
两个实用技巧:
- 以
n_bytes = 0调用可查询所需缓冲大小(此时buffer可为NULL)。文档特别提醒:这只是"足够大的缓冲大小",可能比严格必要略大,不能用于精确计算位数; - 两步法读取任意大小整数——先问大小、再分配、再拷贝,官方示例如下:
// 第一步:询问需要多大缓冲
Py_ssize_t expected = PyLong_AsNativeBytes(pylong, NULL, 0, -1);
if (expected < 0) {
// 失败,已设置 Python 异常
return NULL;
}
assert(expected != 0); // 按 API 定义不可能为 0
uint8_t *bignum = malloc(expected);
if (!bignum) {
PyErr_SetString(PyExc_MemoryError, "bignum malloc failed.");
return NULL;
}
// 第二步:安全取走完整值
Py_ssize_t bytes = PyLong_AsNativeBytes(pylong, bignum, expected, -1);
if (bytes < 0) { // 已设置异常
free(bignum);
return NULL;
}
else if (bytes > expected) { // 预检查后理论上不可能
PyErr_SetString(PyExc_RuntimeError,
"Unexpected bignum truncation after a size check.");
free(bignum);
return NULL;
}
// ... 使用 bignum ...
free(bignum);
最简单的"转 C 定宽变量"用法:
int32_t value;
Py_ssize_t bytes = PyLong_AsNativeBytes(pylong, &value, sizeof(value), -1);
if (bytes < 0) {
return NULL; // 失败,已设置异常
}
// bytes <= 4 表示完整写入;bytes > 4 表示 value 是被截断的低位
5.2 flags 标志表
flags 取 -1(即 Py_ASNATIVEBYTES_DEFAULTS,等价于 NATIVE_ENDIAN | UNSIGNED_BUFFER)或下列标志的组合;-1 不能与其他标志混用:
| 标志 | 值 | 含义 |
|---|---|---|
Py_ASNATIVEBYTES_DEFAULTS |
-1 |
类 C 强转默认行为 |
Py_ASNATIVEBYTES_BIG_ENDIAN |
0 |
大端(最高有效字节写入 buffer 所指地址) |
Py_ASNATIVEBYTES_LITTLE_ENDIAN |
1 |
小端 |
Py_ASNATIVEBYTES_NATIVE_ENDIAN |
3 |
CPython 编译时的原生字节序,覆盖其他字节序标志;取值 2 被保留 |
Py_ASNATIVEBYTES_UNSIGNED_BUFFER |
4 |
目标按无符号解释:尺寸计算省略符号位(例如 128 可放进 1 字节缓冲);但不影响负值的处理——负值始终要求至少一个符号位空间 |
Py_ASNATIVEBYTES_REJECT_NEGATIVE |
8 |
输入为负时设置异常;未设置时负值只要有一个符号位空间就会被拷贝(与 UNSIGNED_BUFFER 无关) |
Py_ASNATIVEBYTES_ALLOW_INDEX |
16 |
非整数输入先调用 __index__——这会执行 Python 代码、可能放行动作,故 flags = -1 时该位不置位,非整数直接 TypeError |
值始终以二进制补码复制。默认 flags 下的一个易错点(文档专门给了 note):多个 Python 整数可映射到同一缓冲值——255 和 -1 都能放进 1 字节缓冲且置满所有位,这正是典型 C 强转行为。
PyLong_FromNativeBytes 把缓冲前 n_bytes 字节按补码有符号数解释为 Python 整数(n_bytes = 0 恒得 0);PyLong_FromUnsignedNativeBytes 则按无符号解释、结果恒非负。两者的 flags 只认字节序选择与"强制无符号",其余标志被忽略;-1 表示"原生字节序 + 按最高位是否为符号位处理"。
这些标志常量在 Include/longobject.h 中有对应 #define,且该头文件的注释块与文档逐字对应,可直接在扩展中 #include <longobject.h> 使用(PyLong_AsInt、FromInt32 等 3.13/3.14 函数受 Py_LIMITED_API 版本门控)。
六、Export API(3.14):不依赖内部布局地读写大整数
这是文档最后、也是演进方向最明确的一节。CPython 历史上让扩展直接读 PyLongObject 的 ob_digit 数组与 PyLong_SHIFT/PyLong_BASE/PyLong_MASK 宏,但内部表示一旦变化,这类代码全部失效。3.14 正式提供导出/导入 API,目标是"即使 CPython 的内部整数表示改变,它们也能继续正确工作"。
6.1 PyLongLayout 与 PyLong_GetNativeLayout
PyLongLayout 描述"数字(digit,即 GMP 术语中的 limb)数组"的布局,用于承载任意精度整数的绝对值:
typedef struct PyLongLayout {
uint8_t bits_per_digit; // 每数字位数,如 15 表示 bit 0-14 有效
uint8_t digit_size; // 每数字字节数(15 位数字至少占 2 字节)
int8_t digits_order; // 1:最高有效数字在前;-1:最低有效数字在前
int8_t digit_endianness; // 1:数字内大端;-1:数字内小端
} PyLongLayout;
const PyLongLayout* PyLong_GetNativeLayout(void) 返回 Python int 的原生布局。约束:不得在 Python 初始化之前或终结之后调用;返回的布局在 Python 终结前有效;同一进程内所有子解释器布局相同,因此可以缓存。Python 侧可参见 sys.int_info。
6.2 PyLongExport / PyLong_Export / PyLong_FreeExport
typedef struct PyLongExport {
int64_t value; // 仅 digits 为 NULL 时有效
uint8_t negative; // 仅 digits 非 NULL 时有效:1 表示负数
Py_ssize_t ndigits; // digits 数组中的数字个数
const void *digits; // 只读无符号数字数组,可为 NULL
} PyLongExport;
int PyLong_Export(PyObject *obj, PyLongExport *export_long);
void PyLong_FreeExport(PyLongExport *export_long);
导出结果分两种情形:
digits == NULL:整数值可装进int64_t,只读value成员即可——这是"小整数"的快速路径;digits != NULL:使用negative+ndigits+digits三件套,配合PyLong_GetNativeLayout()解释数组。
使用纪律:export_long 必须是调用者分配的、非 NULL 指针;成功返回 0 并填充结构,出错设异常返回 -1;不再需要时必须调用 PyLong_FreeExport 释放(实现细节:digits 为 NULL 时该调用是可选的)。对 Python int 对象或其子类,此函数总是成功。
6.3 PyLongWriter API:反向导入
PyLongWriter 用于"由数字数组构造 Python int",实例必须由 PyLongWriter_Finish 或 PyLongWriter_Discard 销毁:
PyLongWriter *PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits);
PyObject *PyLongWriter_Finish(PyLongWriter *writer);
void PyLongWriter_Discard(PyLongWriter *writer);
PyLongWriter_Create:成功时分配*digits并返回 writer;出错设异常返回NULL。negative取 1/0;ndigits必须大于 0;digits参数本身不得为NULL。随后调用者按PyLong_GetNativeLayout描述的布局填充数字,每个数字必须在[0, (1 << bits_per_digit) - 1]内,未使用的高位数字必须置 0;PyLongWriter_Finish:负责规范化数字并在必要时转换为紧凑整数,成功返回 Pythonint,出错设异常返回NULL;调用后 writer 与digits数组均失效;PyLongWriter_Discard:不创建 int 对象直接销毁 writer;writer为NULL时不执行任何操作。
这套 API 适合 C 扩展内部做大数运算(如手写分治乘法)后把结果搬回 Python 层的场景,完全绕开内部头文件。
七、弃用 API:PyLong_SHIFT / PyLong_BASE / PyLong_MASK
文档以独立小节列出三个"描述 PyLongObject 内部表示参数"的宏,均标记为软弃用:
| 弃用宏 | 等价物 |
|---|---|
PyLong_SHIFT |
PyLong_GetNativeLayout() 输出的 bits_per_digit |
PyLong_BASE |
当前等价 1 << PyLong_SHIFT |
PyLong_MASK |
当前等价 (1 << PyLong_SHIFT) - 1 |
迁移指引一句话:读整数数据改用 PyLong_GetNativeLayout + PyLong_Export,写整数数据改用 PyLongWriter。这些宏当前仍与导出 API 使用同一布局,但导出 API 是为"内部表示将来改变"而设计的前向兼容路径。
八、工程实践清单
结合文档约束与 Objects/longobject.c 中的实际实现(PyLong_Export、PyLongWriter_Create、PyLong_GetNativeLayout、PyUnstable_Long_IsCompact 等均在此文件内定义),给 C 扩展开发者一份可核对的实践清单:
- 每个
PyLong_As*返回值都要做错误检查,并理解该函数的失败哨兵(有符号是-1,无符号是(T)-1,AsDouble是-1.0);无法确定值是否合法时,用PyLong_AsLongAndOverflow类 API 把"真 -1"与"溢出/错误"区分开; int子类与__index__:3.10 起PyLong_As*系列只认__index__不再回退__int__,依赖布尔值(bool是int子类)或自定义__index__对象的地方行为是确定的;- 需要跨版本可移植地访问大整数位级数据时,用 3.14 的 Export/Writer API 而非
ob_digit+PyLong_SHIFT,后者只是过渡期兼容层; - 小整数快路径用
PyUnstable_Long_IsCompact+PyLongWriter/CompactValue,但要接受其PyUnstable前缀承诺的"不保证稳定"; - 定宽数值边界优先用 3.14 的
PyLong_AsInt32/64、PyLong_AsUInt32/64(返回码 + 输出参数,语义清晰无-1歧义),3.13 用PyLong_AsInt与PyLong_AsNativeBytes; - 进程 ID 等系统类型使用
PyLong_FromPid/PyLong_AsPid宏(3.2 添加),它们按sizeof(pid_t)自动别名到正确的long/long long/int版本,避免手工判断平台宽度。
以上即 Doc/c-api/long.rst 定义的整数对象 C API 全貌:从单值互转、原生字节转换,到 3.14 的布局导出与 PyLongWriter 导入,构成了一条与 CPython 内部表示解耦、可随版本演进的完整互操作路径。
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 StartedRust0624
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