CPython C 扩展参数解析与返回值构建:PyArg_Parse 系列与 Py_BuildValue 格式串全解
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本文基于 CPython 官方文档 Doc/c-api/arg.rst 展开,系统讲解 C 扩展开发中“参数解析”(PyArg_Parse*家族)与“返回值构建”(Py_BuildValue)两大核心机制。读完本文,你将理解格式串(format string)的完整语法、字符串/缓冲区的三种内存语义(Py_buffer借用、分配缓冲、裸指针借用)、所有数字与对象格式单元、|、$、:、;等特殊控制字符的用法,并能结合 Python/getargs.c 与 Python/modsupport.c 的源码印证底层实现,写出可复制、可运行的 C 扩展代码。
一、总体模型:格式串驱动的参数解析
在编写 C 扩展函数时,Python 调用层会把参数打包传递给你:传统METH_VARARGS调用约定传一个PyObject *args元组;METH_VARARGS | METH_KEYWORDS额外传关键字字典;METH_FASTCALL则传一个参数指针数组加计数。PyArg_Parse*系列函数的职责,就是把这堆PyObject*按你指定的“格式串”逐个转换、存进 C 局部变量。
文档明确了三个入口都使用同一套格式串语法:
PyArg_ParseTuple—— 解析只有位置参数的函数参数(args元组);PyArg_ParseTupleAndKeywords—— 解析位置 + 关键字参数;PyArg_Parse—— 解析单个位置参数(配合METH_O调用约定)。
格式串由零个或多个“格式单元”(format unit)组成。每个格式单元描述一个 Python 对象,通常是一个单独字符,也可以是括号括起来的格式单元序列。除少数例外,一个非嵌套括号的格式单元对应一个 C 侧的“地址参数”——即你传入的局部变量的地址。文档约定:引号形式是格式单元,圆括号内是匹配的 Python 对象类型,方括号内是应传入地址的 C 变量类型。
从源码结构看,这三个函数最终都汇聚到 Python/getargs.c 中的vgetargs1_impl(变参被展平为va_list),再由convertsimple逐字符分发处理(见 Python/getargs.c#L712)。PyArg_Parse走vgetargs1(args, format, &va, FLAG_COMPAT)分支(Python/getargs.c#L78-L87),PyArg_ParseTuple走vgetargs1(..., 0)(Python/getargs.c#L103-L112)。
转换成功/失败的语义:转换成功要求arg对象与格式完全匹配,且格式串必须被完整耗尽。成功时函数返回非零(true),失败时返回 0 并抛出相应异常。当某个格式单元转换失败时,该单元及其后所有格式单元对应的 C 变量都保持原值不变——你无需手动清理已写入的变量。
二、字符串与缓冲区:三种内存语义
文档将“字符串/缓冲区转 C”归为三类,这是 C 扩展中最容易踩内存坑的地方,必须分清各自的释放责任:
y*、s*等填充Py_buffer的格式:它们会锁定(lock)底层缓冲区,使你在Py_BEGIN_ALLOW_THREADS代码块中使用时也不会遇到可变数据被扩容或销毁的风险。作为代价,你必须在处理完毕后(包括任何提前退出路径)调用PyBuffer_Release释放。es、es#、et、et#:由PyArg_ParseTuple负责分配结果缓冲区。你必须在处理完毕后调用PyMem_Free释放。- “借用”缓冲(borrowed buffer):其余格式(如
s、s#、y、y#)接收str或只读 bytes-like 对象,直接给出const char *裸指针。该缓冲区由对应 Python 对象管理,生命周期与对象一致,你不需要释放任何内存。
第 3 类“借用”有两层安全约束,文档说得很明确:
- 对象的
PyBufferProcs.bf_releasebuffer字段必须为NULL。这排除了常见的可变对象如bytearray,也排除了某些只读对象(例如指向bytes的memoryview); - 除此之外,CPython不检查输入对象是否真的不可变(例如它是否会响应可写缓冲请求,或另一个线程是否可能修改数据)。
注意:在 Python 3.12 及更早版本中,若要使用所有
#变体格式(s#、y#等),必须在#include "Python.h"之前定义宏PY_SSIZE_T_CLEAN;Python 3.13 及之后不再需要。
2.1 字符串/缓冲区格式单元速查
| 格式单元 | 接受的 Python 类型 | C 变量类型 | 说明 |
|---|---|---|---|
s | str | const char * | 转换为 NUL 结尾的 UTF-8 C 字符串;含内嵌 NUL 时抛ValueError;编码失败抛UnicodeError。不接受 bytes-like 对象 |
s* | str或 bytes-like | Py_buffer | 接受 Unicode 与 bytes-like,可含内嵌 NUL,需PyBuffer_Release |
s# | str、只读 bytes-like | const char *、Py_ssize_t | 借用缓冲,指针 + 长度两个变量,可含内嵌 NUL |
z | str或None | const char * | 同s,但None时指针置NULL |
z* | str、bytes-like 或None | Py_buffer | 同s*,None时buf成员为NULL |
z# | str、只读 bytes-like 或None | const char *、Py_ssize_t | 同s#,None时指针为NULL |
y | 只读 bytes-like | const char * | 不接受 Unicode;含内嵌 NUL 抛ValueError |
y* | bytes-like | Py_buffer | 官方推荐接收二进制数据的方式 |
y# | 只读 bytes-like | const char *、Py_ssize_t | 同s#但仅限 bytes-like |
S | bytes | PyBytesObject *(或PyObject *) | 严格类型检查,不做转换,非 bytes 抛TypeError |
Y | bytearray | PyByteArrayObject *(或PyObject *) | 同上,严格bytearray |
U | str | PyObject * | 严格 Unicode 检查,不做转换 |
w* | 可读写 bytes-like | Py_buffer | 接受实现可读写缓冲接口的对象,需PyBuffer_Release |
S、Y、U的共同点是只验证类型、不做任何转换,因此 C 变量直接拿到对应对象指针,C 侧也可直接声明为PyObject*。
关于s的补充(来自文档的 note):s不接受 bytes-like 对象。如果你要接收文件系统路径并转成 C 字符串,更合适的是用O&格式配合PyUnicode_FSConverter作为 converter(3.5 之前的版本对内嵌 NUL 抛TypeError,3.5 起改为ValueError)。
2.2es/et/es#/et#:显式指定编码
es(str→const char *encoding, char **buffer):把 Unicode 编码成字符缓冲,只支持不含内嵌 NUL 的编码结果。它需要两个 C 参数:
- 第一个仅作输入:指向编码名的 NUL 结尾 C 字符串,或
NULL(表示用'utf-8');指定了 Python 不认识的编码会抛异常; - 第二个必须是
char **:解析后指向编码结果的缓冲区。PyArg_ParseTuple会分配恰好需要的空间、拷贝数据并调整指针——调用方负责用PyMem_Free释放。
et(str/bytes/bytearray→ 同上):与es相同,但字节串对象不经重新编码直接透传,实现上假定该字节串对象已经使用了你传入的参数编码。
es#(str→const char *encoding, char **buffer, Py_ssize_t *buffer_length):与es的区别是允许输入含 NUL 字符。第三个参数是指向整数的指针,被设置为输出缓冲的字节数。它有两种工作模式:
- 若
*buffer初始为NULL:函数分配所需缓冲区并拷贝,调用方须PyMem_Free; - 若
*buffer指向已分配的缓冲区:直接使用该内存,并把*buffer_length的初始值解释为缓冲区容量,拷贝并 NUL 结尾;容量不足时抛ValueError。
两种模式下*buffer_length最终都设置为编码数据的长度(不含结尾 NUL 字节)。et#与es#相同,只是字节串对象直接透传不重编码。
从源码实现印证:这些“需要清理”的分配(Py_buffer与char **)在 Python/getargs.c 中通过cleanup_ptr(内部调PyMem_Free,Python/getargs.c#L202-L209)与cleanup_buffer(内部调PyBuffer_Release,Python/getargs.c#L211-L219)登记到 freelist;一旦解析中途失败,cleanreturn(Python/getargs.c#L235-L252)会自动执行已登记的清理函数,避免异常路径下的内存泄漏——这正是文档所说“或任何提前退出情形”背后有兜底的原因。
此外,3.12 起u、u#、Z、Z#已被移除,因为它们依赖遗留的Py_UNICODE*表示。
2.3 借用引用的通用规则
文档专门强调:传给调用方的任何 Python 对象引用都是借用引用(borrowed reference),不要释放它们(即不要减少引用计数)。同样,额外传入这些函数的参数必须是“类型由格式串决定的变量”的地址,用来存放输入元组中的值;只有少数格式单元(上文的es、es#等)把额外参数当输入用,此时必须与文档对应条目匹配。
三、数字格式:整数、字符与浮点
数字格式把 Python 数字(或单字符)表示为 C 数字。要求int、float、complex的格式也可以调用对象对应的__index__、__float__或__complex__方法完成转换。范围语义上:
- 有符号整数格式:值超出 C 类型范围抛
OverflowError; - 无符号整数格式:接收域太小时最高位静默截断,当值大于 C 类型最大值或小于同尺寸有符号类型的最小值时发出
DeprecationWarning。
完整对照表(引号格式单元 / 接受类型 / C 变量类型):
| 格式单元 | Python 类型 | C 变量类型 | 说明 |
|---|---|---|---|
b | int | unsigned char | 非负整数转无符号 tiny int |
B | int | unsigned char | 不做溢出检查 |
h | int | short int | |
H | int | unsigned short int | |
i | int | int | |
I | int | unsigned int | |
l | int | long int | |
k | int | unsigned long | 3.14 起可用__index__ |
L | int | long long | |
K | int | unsigned long long | 3.14 起可用__index__ |
n | int | Py_ssize_t | 首选的“平台指针尺寸整数” |
c | 长度为 1 的bytes或bytearray | char | 3.3 起允许bytearray |
C | 长度为 1 的str | int | |
f | float | float | |
d | float | double | |
D | complex | Py_complex |
注意 3.15 起,对无符号格式B、H、I、k、K,当值超出范围时会发出DeprecationWarning(文档标记为 deprecated 行为预警)。源码印证:Python/getargs.c 的convertsimple中,b用PyLong_AsLong后手动检查< 0与> UCHAR_MAX抛OverflowError,而B走PyLong_AsNativeBytes且对超宽值调PyErr_WarnEx(PyExc_DeprecationWarning, "integer value out of range", 1)(Python/getargs.c#L712-L780),与文档描述逐字对应。
四、其他对象格式:O、O!、O&、p与嵌套元组
4.1O与O!
O(object →PyObject *):把 Python 对象原样存入 C 对象指针,不创建新强引用(引用计数不增加),存入的指针非NULL。O!(object →typeobject,PyObject *):与O类似但取两个 C 参数:第一个是 Python 类型对象的地址,第二个是存放对象指针的PyObject*变量地址;类型不符抛TypeError。
4.2O&:自定义转换器
O&通过converter函数把 Python 对象转成任意类型的 C 变量。它取两个参数:converter 函数本身,以及目标 C 变量地址(转成void *)。converter 的调用协议是:
status = converter(object, address);object是待转换对象,address是传给PyArg_Parse*的void*。成功返回 1,失败返回 0(且 converter 应抛出异常并保持address内容不变)。官方示例 converter:PyUnicode_FSConverter与PyUnicode_FSDecoder。
Py_CLEANUP_SUPPORTED机制:如果 converter 返回Py_CLEANUP_SUPPORTED标记,当参数解析最终失败时,它可能被第二次调用以释放已分配的内存——第二次调用时object参数为NULL,address与第一次相同。
4.3p与(items)
p(bool→int,3.3 加入):对传入值做真值测试(predicate),结果为真置 1、假置 0。接受任何合法的 Python 值(真值语义见文档 truth 一节)。(items)(sequence → 对应matching-items):对象必须是 Python 序列(str、bytes、bytearray除外),长度必须等于items中格式单元的数量,C 参数须与items的单元一一对应,且允许嵌套。两条安全约束:若items内含存借用缓冲的单元(
s、s#、z、z#、y、y#)或借用引用的单元(S、Y、U、O、O!),则该对象必须是 tuple(3.14 起str与bytearray不再被接受为序列;3.14 同时把“含借用单元时用非 tuple 序列”标记为 deprecated)。items中O&的 converter 不得存储借用缓冲或借用引用。
4.4 特殊控制字符:|、$、:、;
这些字符不能出现在嵌套括号内:
|:其后的参数变为可选。可选参数对应的 C 变量必须预先初始化为默认值——当调用方没有提供该参数时,PyArg_ParseTuple不会触碰这些变量。例如"OO|OO"对应 Python 签名f(a, b, c=None, d=None)。$(仅PyArg_ParseTupleAndKeywords,3.3 加入):其后的参数变为 keyword-only。若$之前出现过|则它们是可选的,否则是必需的;|不能出现在$之后。例如"O|O$O"对应f(a, b=None, *, c=None),"OO$OO"对应f(a, b, *, c, d)。::格式单元列表到此结束,冒号后的字符串用作错误信息中的函数名(即PyArg_ParseTuple抛出异常的“关联值”)。实践中强烈建议总是加上,例如"i:my_function",这样报错信息会写成my_function() argument must be ...。;:格式单元列表到此结束,分号后的字符串整体替代默认错误信息。:与;互斥。
五、API 函数族一览
5.1 解析函数
int PyArg_ParseTuple(PyObject *args, const char *format, ...); int PyArg_VaParse(PyObject *args, const char *format, va_list vargs); int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char * const *keywords, ...); int PyArg_VaParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char * const *keywords, va_list vargs); int PyArg_ValidateKeywordArguments(PyObject *); int PyArg_Parse(PyObject *args, const char *format, ...); int PyArg_ParseArray(PyObject *const *args, Py_ssize_t nargs, const char *format, ...); // 3.15+ int PyArg_ParseArrayAndKeywords(PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames, const char *format, const char * const *kwlist, ...); // 3.15+PyArg_VaParse/PyArg_VaParseTupleAndKeywords与变参版本功能相同,只是接受va_list。PyArg_ParseTupleAndKeywords的keywords参数是NULL 结尾的关键字参数名数组(NUL 结尾的 ASCII/UTF-8 C 字符串);空字符串名表示 positional-only 参数(3.6 加入)。- 版本变化要点:3.13 起
keywords参数类型在 C 中是char * const *、C++ 中是const char * const *(不再是char **),并支持非 ASCII 关键字参数名;可用PY_CXX_CONST宏覆盖该前缀(C 默认为空、C++ 默认为const,在包含Python.h前定义即可覆盖,3.13 加入)。 PyArg_ValidateKeywordArguments(3.2 加入):确保关键字字典的键都是字符串。只有在你不用PyArg_ParseTupleAndKeywords(它已内置该检查)时才需要。PyArg_Parse:解析单个位置参数,面向METH_O调用约定。文档给出的官方示例:
// Function using METH_O calling convention static PyObject* my_function(PyObject *module, PyObject *arg) { int value; if (!PyArg_Parse(arg, "i:my_function", &value)) { return NULL; } // ... use value ... }PyArg_ParseArray/PyArg_ParseArrayAndKeywords(均为 3.15 加入):分别解析METH_FASTCALL与METH_FASTCALL | METH_KEYWORDS约定下的数组参数(PyObject *const *args+nargs)以及关键字参数(kwnames+kwlist)。从源码看,它们都收敛到vgetargs1_impl/vgetargskeywords_impl(Python/getargs.c#L139-L170),与元组版本共享同一套格式单元语义。
5.2PyArg_UnpackTuple:不用格式串的简单取参
int PyArg_UnpackTuple(PyObject *args, const char *name, Py_ssize_t min, Py_ssize_t max, ...);这是一种“简单取参”形式:不使用格式串指定类型。使用它的函数应在函数/方法表中声明为METH_VARARGS;args必须是元组,长度至少min、至多max(两者可以相等)。额外参数各是一个指向PyObject*变量的指针,将被填入args中对应的值——注意是借用引用。未提供的可选参数对应的变量不会被写入,应由调用方预初始化。args不是元组或元素数量不对时返回 false 并设置异常。
文档引用自_weakref辅助模块的源码示例:
static PyObject * weakref_ref(PyObject *self, PyObject *args) { PyObject *object; PyObject *callback = NULL; PyObject *result = NULL; if (PyArg_UnpackTuple(args, "ref", 1, 2, &object, &callback)) { result = PyWeakref_NewRef(object, callback); } return result; }文档指出,这个调用与下面的PyArg_ParseTuple调用完全等价:
PyArg_ParseTuple(args, "O|O:ref", &object, &callback)源码印证:PyArg_UnpackTuple实现于 Python/getargs.c#L2898,失败分支会设置"PyArg_UnpackTuple() argument list is not a tuple"错误,与文档描述一致。
六、构建返回值:Py_BuildValue与Py_VaBuildValue
6.1 基本契约
PyObject* Py_BuildValue(const char *format, ...); PyObject* Py_VaBuildValue(const char *format, va_list vargs);Py_BuildValue用与PyArg_Parse*相似的格式串 + 一组值创建新的 Python 值;出错返回NULL且异常已置位。Py_VaBuildValue与之相同,只是接受va_list。从源码看,Py_BuildValue直接转发到va_build_value(Python/modsupport.c#L496-L503),非法格式字符会触发"bad format char passed to Py_BuildValue"的 SystemError 路径(Python/modsupport.c#L487)。
三条易被忽略的契约:
- 不总是返回 tuple:只有格式串含两个及以上格式单元时才构建 tuple;空格式串返回
None;恰好一个单元时返回该单元描述的对象本身。要强制得到 0 元或 1 元 tuple,请给格式串加括号,如"(i)"。 - 缓冲区是拷贝而非引用:以
s、s#等格式提供的内存缓冲,其数据会被拷贝;Py_BuildValue创建的对象从不引用调用方的缓冲。换言之,若你malloc后把内存传给Py_BuildValue,Py_BuildValue返回后由你负责free该内存。 - 格式串中的空格、制表符、冒号和逗号被忽略(
s#这类单元内部除外),可用来提高长格式串的可读性。
6.2 构建格式单元对照表
| 格式单元 | 返回的 Python 类型 | C 参数类型 | 说明 |
|---|---|---|---|
s | str或None | const char * | NUL 结尾 C 串按'utf-8'解码;指针为NULL时得None |
s# | str或None | const char *、Py_ssize_t | 串 + 长度;NULL时忽略长度得None |
y | bytes | const char * | C 串转bytes;NULL得None |
y# | bytes | const char *、Py_ssize_t | C 串 + 长度;NULL得None |
z/z# | str或None | 同s/s# | 与s/s#相同 |
u/u# | str | const wchar_t *(+ 长度) | 宽字符缓冲(UTF-16 或 UCS-4)转 Unicode;NULL得None |
U/U# | str或None | 同s/s# | 与s/s#相同 |
i | int | int | |
b | int | char | |
h | int | short int | |
l | int | long int | |
B | int | unsigned char | |
H | int | unsigned short int | |
I | int | unsigned int | |
k | int | unsigned long | |
L | int | long long | |
K | int | unsigned long long | |
n | int | Py_ssize_t | |
p | bool | int | 必须传 int;变参不做自动类型收缩,其他类型可用(x) ? 1 : 0或!!x转换(3.14 加入) |
c | 长度 1 的bytes | char | 表示一个字节 |
C | 长度 1 的str | int | 表示一个字符 |
d/f | float | double/float | |
D | complex | Py_complex * | 注意传结构体地址 |
O | object | PyObject * | 原样传递但创建新强引用(引用计数 +1);传入NULL时假定上游出错并已置异常——Py_BuildValue返回NULL但不抛新异常;若尚无异常则置SystemError |
S | object | PyObject * | 同O |
N | object | PyObject * | 同O但不创建新强引用;适合对象由参数列表中的构造器调用创建的情形(如Py_BuildValue("N", obj)把所有权交给返回值) |
O& | object | converter,anything | 通过 converter 把anything(应与void*兼容)转为新 Python 对象或NULL |
(items) | tuple | 对应 C 值 | 构建等长元组 |
[items] | list | 对应 C 值 | 构建等长列表 |
{items} | dict | 成对 C 值 | 每连续两个值构成一对键值 |
格式串本身有语法错误时,置SystemError并返回NULL。
七、实战要点小结(对应文档结论)
- 选对函数:只有位置参数用
PyArg_ParseTuple;位置 + 关键字用PyArg_ParseTupleAndKeywords(记住|/$语义与空名 positional-only);METH_O单参数用PyArg_Parse;METH_FASTCALL用 3.15 的PyArg_ParseArray/PyArg_ParseArrayAndKeywords;不想引入类型转换就用PyArg_UnpackTuple。 - 格式串尾随
:函数名几乎总是值得写,它直接决定用户看到什么报错;需要完全自定义错误文案时用;message替代(二者互斥)。 - 可选参数先赋默认值:
|之后的变量在调用方省略时不会被写入。 - 分清三种内存语义:
Py_buffer系(s*/y*/w*/z*)用PyBuffer_Release收尾;es/es#/et/et#用PyMem_Free收尾;借用指针(s/s#/y/y#/z系)不释放但生命周期依赖源对象;解析器自身的失败路径会自动走 freelist 兜底清理(见 Python/getargs.c#L200-L252)。 Py_BuildValue的O/N选择:已有引用、想移交所有权用N;普通对象引用、需要 +1 强引用用O;NULL参数的语义是“上游已出错”的哨兵。- 更多扩展函数与方法的上下文示例可参阅 Doc/extending/index.rst;格式串的公共声明位于 Include/modsupport.h(如
PyArg_ParseTuple的原型声明),实现主体在 Python/getargs.c(解析)与 Python/modsupport.c(构建)。
适用前提:以上版本行为(3.13 的
keywords类型变化、3.14 的k/K支持__index__、3.15 的PyArg_ParseArray*与无符号溢出DeprecationWarning等)均以当前仓库(CPython 主干,对应 3.15+ 开发版本)文档与源码为准,移植到 3.13/3.14 及以下运行时请核对对应版本的差异。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考