news 2026/9/7 3:27:49

CPython C 扩展参数解析与返回值构建:PyArg_Parse 系列与 Py_BuildValue 格式串全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPython C 扩展参数解析与返回值构建:PyArg_Parse 系列与 Py_BuildValue 格式串全解

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_Parsevgetargs1(args, format, &va, FLAG_COMPAT)分支(Python/getargs.c#L78-L87),PyArg_ParseTuplevgetargs1(..., 0)(Python/getargs.c#L103-L112)。

转换成功/失败的语义:转换成功要求arg对象与格式完全匹配,且格式串必须被完整耗尽。成功时函数返回非零(true),失败时返回 0 并抛出相应异常。当某个格式单元转换失败时,该单元及其后所有格式单元对应的 C 变量都保持原值不变——你无需手动清理已写入的变量。

二、字符串与缓冲区:三种内存语义

文档将“字符串/缓冲区转 C”归为三类,这是 C 扩展中最容易踩内存坑的地方,必须分清各自的释放责任:

  1. y*s*等填充Py_buffer的格式:它们会锁定(lock)底层缓冲区,使你在Py_BEGIN_ALLOW_THREADS代码块中使用时也不会遇到可变数据被扩容或销毁的风险。作为代价,你必须在处理完毕后(包括任何提前退出路径)调用PyBuffer_Release释放。
  2. eses#etet#:由PyArg_ParseTuple负责分配结果缓冲区。你必须在处理完毕后调用PyMem_Free释放。
  3. “借用”缓冲(borrowed buffer):其余格式(如ss#yy#)接收str或只读 bytes-like 对象,直接给出const char *裸指针。该缓冲区由对应 Python 对象管理,生命周期与对象一致,你不需要释放任何内存。

第 3 类“借用”有两层安全约束,文档说得很明确:

  • 对象的PyBufferProcs.bf_releasebuffer字段必须为NULL。这排除了常见的可变对象如bytearray,也排除了某些只读对象(例如指向bytesmemoryview);
  • 除此之外,CPython不检查输入对象是否真的不可变(例如它是否会响应可写缓冲请求,或另一个线程是否可能修改数据)。

注意:在 Python 3.12 及更早版本中,若要使用所有#变体格式(s#y#等),必须在#include "Python.h"之前定义宏PY_SSIZE_T_CLEAN;Python 3.13 及之后不再需要。

2.1 字符串/缓冲区格式单元速查

格式单元接受的 Python 类型C 变量类型说明
sstrconst char *转换为 NUL 结尾的 UTF-8 C 字符串;含内嵌 NUL 时抛ValueError;编码失败抛UnicodeError不接受 bytes-like 对象
s*str或 bytes-likePy_buffer接受 Unicode 与 bytes-like,可含内嵌 NUL,需PyBuffer_Release
s#str、只读 bytes-likeconst char *Py_ssize_t借用缓冲,指针 + 长度两个变量,可含内嵌 NUL
zstrNoneconst char *s,但None时指针置NULL
z*str、bytes-like 或NonePy_buffers*Nonebuf成员为NULL
z#str、只读 bytes-like 或Noneconst char *Py_ssize_ts#None时指针为NULL
y只读 bytes-likeconst char *不接受 Unicode;含内嵌 NUL 抛ValueError
y*bytes-likePy_buffer官方推荐接收二进制数据的方式
y#只读 bytes-likeconst char *Py_ssize_ts#但仅限 bytes-like
SbytesPyBytesObject *(或PyObject *严格类型检查,不做转换,非 bytes 抛TypeError
YbytearrayPyByteArrayObject *(或PyObject *同上,严格bytearray
UstrPyObject *严格 Unicode 检查,不做转换
w*可读写 bytes-likePy_buffer接受实现可读写缓冲接口的对象,需PyBuffer_Release

SYU的共同点是只验证类型、不做任何转换,因此 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#:显式指定编码

esstrconst char *encoding, char **buffer):把 Unicode 编码成字符缓冲,只支持不含内嵌 NUL 的编码结果。它需要两个 C 参数:

  1. 第一个仅作输入:指向编码名的 NUL 结尾 C 字符串,或NULL(表示用'utf-8');指定了 Python 不认识的编码会抛异常;
  2. 第二个必须是char **:解析后指向编码结果的缓冲区。PyArg_ParseTuple会分配恰好需要的空间、拷贝数据并调整指针——调用方负责用PyMem_Free释放

etstr/bytes/bytearray→ 同上):与es相同,但字节串对象不经重新编码直接透传,实现上假定该字节串对象已经使用了你传入的参数编码。

es#strconst 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_bufferchar **)在 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 起uu#ZZ#已被移除,因为它们依赖遗留的Py_UNICODE*表示。

2.3 借用引用的通用规则

文档专门强调:传给调用方的任何 Python 对象引用都是借用引用(borrowed reference),不要释放它们(即不要减少引用计数)。同样,额外传入这些函数的参数必须是“类型由格式串决定的变量”的地址,用来存放输入元组中的值;只有少数格式单元(上文的eses#等)把额外参数当输入用,此时必须与文档对应条目匹配。

三、数字格式:整数、字符与浮点

数字格式把 Python 数字(或单字符)表示为 C 数字。要求intfloatcomplex的格式也可以调用对象对应的__index____float____complex__方法完成转换。范围语义上:

  • 有符号整数格式:值超出 C 类型范围抛OverflowError
  • 无符号整数格式:接收域太小时最高位静默截断,当值大于 C 类型最大值或小于同尺寸有符号类型的最小值时发出DeprecationWarning

完整对照表(引号格式单元 / 接受类型 / C 变量类型):

格式单元Python 类型C 变量类型说明
bintunsigned char非负整数转无符号 tiny int
Bintunsigned char不做溢出检查
hintshort int
Hintunsigned short int
iintint
Iintunsigned int
lintlong int
kintunsigned long3.14 起可用__index__
Lintlong long
Kintunsigned long long3.14 起可用__index__
nintPy_ssize_t首选的“平台指针尺寸整数”
c长度为 1 的bytesbytearraychar3.3 起允许bytearray
C长度为 1 的strint
ffloatfloat
dfloatdouble
DcomplexPy_complex

注意 3.15 起,对无符号格式BHIkK,当值超出范围时会发出DeprecationWarning(文档标记为 deprecated 行为预警)。源码印证:Python/getargs.c 的convertsimple中,bPyLong_AsLong后手动检查< 0> UCHAR_MAXOverflowError,而BPyLong_AsNativeBytes且对超宽值调PyErr_WarnEx(PyExc_DeprecationWarning, "integer value out of range", 1)(Python/getargs.c#L712-L780),与文档描述逐字对应。

四、其他对象格式:OO!O&p与嵌套元组

4.1OO!

  • 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_FSConverterPyUnicode_FSDecoder

Py_CLEANUP_SUPPORTED机制:如果 converter 返回Py_CLEANUP_SUPPORTED标记,当参数解析最终失败时,它可能被第二次调用以释放已分配的内存——第二次调用时object参数为NULLaddress与第一次相同。

4.3p(items)

  • pboolint,3.3 加入):对传入值做真值测试(predicate),结果为真置 1、假置 0。接受任何合法的 Python 值(真值语义见文档 truth 一节)。

  • (items)(sequence → 对应matching-items):对象必须是 Python 序列(strbytesbytearray除外),长度必须等于items中格式单元的数量,C 参数须与items的单元一一对应,且允许嵌套。

    两条安全约束:若items内含存借用缓冲的单元(ss#zz#yy#)或借用引用的单元(SYUOO!),则该对象必须是 tuple(3.14 起strbytearray不再被接受为序列;3.14 同时把“含借用单元时用非 tuple 序列”标记为 deprecated)。itemsO&的 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_ParseTupleAndKeywordskeywords参数是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_FASTCALLMETH_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_VARARGSargs必须是元组,长度至少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_BuildValuePy_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)。

三条易被忽略的契约:

  1. 不总是返回 tuple:只有格式串含两个及以上格式单元时才构建 tuple;空格式串返回None;恰好一个单元时返回该单元描述的对象本身。要强制得到 0 元或 1 元 tuple,请给格式串加括号,如"(i)"
  2. 缓冲区是拷贝而非引用:以ss#等格式提供的内存缓冲,其数据会被拷贝Py_BuildValue创建的对象从不引用调用方的缓冲。换言之,若你malloc后把内存传给Py_BuildValuePy_BuildValue返回后由你负责free该内存。
  3. 格式串中的空格、制表符、冒号和逗号被忽略(s#这类单元内部除外),可用来提高长格式串的可读性。

6.2 构建格式单元对照表

格式单元返回的 Python 类型C 参数类型说明
sstrNoneconst char *NUL 结尾 C 串按'utf-8'解码;指针为NULL时得None
s#strNoneconst char *Py_ssize_t串 + 长度;NULL时忽略长度得None
ybytesconst char *C 串转bytesNULLNone
y#bytesconst char *Py_ssize_tC 串 + 长度;NULLNone
z/z#strNones/s#s/s#相同
u/u#strconst wchar_t *(+ 长度)宽字符缓冲(UTF-16 或 UCS-4)转 Unicode;NULLNone
U/U#strNones/s#s/s#相同
iintint
bintchar
hintshort int
lintlong int
Bintunsigned char
Hintunsigned short int
Iintunsigned int
kintunsigned long
Lintlong long
Kintunsigned long long
nintPy_ssize_t
pboolint必须传 int;变参不做自动类型收缩,其他类型可用(x) ? 1 : 0!!x转换(3.14 加入)
c长度 1 的byteschar表示一个字节
C长度 1 的strint表示一个字符
d/ffloatdouble/float
DcomplexPy_complex *注意传结构体地址
OobjectPyObject *原样传递但创建新强引用(引用计数 +1);传入NULL时假定上游出错并已置异常——Py_BuildValue返回NULL但不抛新异常;若尚无异常则置SystemError
SobjectPyObject *O
NobjectPyObject *O创建新强引用;适合对象由参数列表中的构造器调用创建的情形(如Py_BuildValue("N", obj)把所有权交给返回值)
O&objectconverter,anything通过 converter 把anything(应与void*兼容)转为新 Python 对象或NULL
(items)tuple对应 C 值构建等长元组
[items]list对应 C 值构建等长列表
{items}dict成对 C 值每连续两个值构成一对键值

格式串本身有语法错误时,置SystemError并返回NULL

七、实战要点小结(对应文档结论)

  1. 选对函数:只有位置参数用PyArg_ParseTuple;位置 + 关键字用PyArg_ParseTupleAndKeywords(记住|/$语义与空名 positional-only);METH_O单参数用PyArg_ParseMETH_FASTCALL用 3.15 的PyArg_ParseArray/PyArg_ParseArrayAndKeywords;不想引入类型转换就用PyArg_UnpackTuple
  2. 格式串尾随:函数名几乎总是值得写,它直接决定用户看到什么报错;需要完全自定义错误文案时用;message替代(二者互斥)。
  3. 可选参数先赋默认值|之后的变量在调用方省略时不会被写入。
  4. 分清三种内存语义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)。
  5. Py_BuildValueO/N选择:已有引用、想移交所有权用N;普通对象引用、需要 +1 强引用用ONULL参数的语义是“上游已出错”的哨兵。
  6. 更多扩展函数与方法的上下文示例可参阅 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 3:27:43

老北京铜板美食的技术思维:从MVP到微服务的商业智慧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 3:25:44

音乐节奏彩灯控制器毕设实战:从音频采集到动态节拍识别

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 3:25:22

零基础AI漫剧创作全流程:从分镜设计到项目落地

1. AI漫剧创作到底在做什么&#xff1a;先把整条链路看明白 AI漫剧这个词&#xff0c;最近几个月热度涨得很快。简单说&#xff0c;就是用AI工具批量生成漫画风格的连续剧&#xff0c;每一集几十秒到两三分钟&#xff0c;形式介于动态漫画和短剧之间。它不需要真人演员&#xf…

作者头像 李华