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

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

2026-09-06 19:54:00作者:滑思眉Philip

本文基于 CPython 官方文档 Doc/c-api/arg.rst 展开,系统讲解 C 扩展开发中“参数解析”(PyArg_Parse* 家族)与“返回值构建”(Py_BuildValue)两大核心机制。读完本文,你将理解格式串(format string)的完整语法、字符串/缓冲区的三种内存语义(Py_buffer 借用、分配缓冲、裸指针借用)、所有数字与对象格式单元、|$:; 等特殊控制字符的用法,并能结合 Python/getargs.cPython/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 变量类型 说明
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 strNone const char * s,但 None 时指针置 NULL
z* str、bytes-like 或 None Py_buffer s*Nonebuf 成员为 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

SYU 的共同点是只验证类型、不做任何转换,因此 C 变量直接拿到对应对象指针,C 侧也可直接声明为 PyObject*

关于 s 的补充(来自文档的 note):s 不接受 bytes-like 对象。如果你要接收文件系统路径并转成 C 字符串,更合适的是用 O& 格式配合 PyUnicode_FSConverter 作为 converter(3.5 之前的版本对内嵌 NUL 抛 TypeError,3.5 起改为 ValueError)。

2.2 es / 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_FreePython/getargs.c#L202-L209)与 cleanup_buffer(内部调 PyBuffer_ReleasePython/getargs.c#L211-L219)登记到 freelist;一旦解析中途失败,cleanreturnPython/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 变量类型 说明
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 的 bytesbytearray char 3.3 起允许 bytearray
C 长度为 1 的 str int
f float float
d float double
D complex Py_complex

注意 3.15 起,对无符号格式 BHIkK,当值超出范围时会发出 DeprecationWarning(文档标记为 deprecated 行为预警)。源码印证:Python/getargs.cconvertsimple 中,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.1 OO!

  • O(object → PyObject *):把 Python 对象原样存入 C 对象指针,不创建新强引用(引用计数不增加),存入的指针非 NULL
  • O!(object → typeobject, PyObject *):与 O 类似但取两个 C 参数:第一个是 Python 类型对象的地址,第二个是存放对象指针的 PyObject* 变量地址;类型不符抛 TypeError

4.2 O&:自定义转换器

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.3 p(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_implPython/getargs.c#L139-L170),与元组版本共享同一套格式单元语义。

5.2 PyArg_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_valuePython/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 参数类型 说明
s strNone const char * NUL 结尾 C 串按 'utf-8' 解码;指针为 NULL 时得 None
s# strNone const char *Py_ssize_t 串 + 长度;NULL 时忽略长度得 None
y bytes const char * C 串转 bytesNULLNone
y# bytes const char *Py_ssize_t C 串 + 长度;NULLNone
z / z# strNone s / s# s / s# 相同
u / u# str const wchar_t *(+ 长度) 宽字符缓冲(UTF-16 或 UCS-4)转 Unicode;NULLNone
U / U# strNone 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

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

  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 及以下运行时请核对对应版本的差异。

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