CPython 调用协议完全指南:tp_call、Vectorcall(PEP 590)与 Object Calling API 深度解析
CPython 的 C API 为扩展模块提供了两套"调用(call)"协议:经典的 tp_call 协议与 3.9 年引入的 vectorcall 协议(PEP 590),它们决定了 C 扩展如何触发 Python 对象、以及一次调用在底层会经过哪些参数转换。读完本文,你将掌握两套协议的工作机制、PY_VECTORCALL_ARGUMENTS_OFFSET 位标记的原理、完整的 Object Calling API 函数族(PyObject_Call 到 PyObject_VectorcallMethod)的选型依据,并能结合 Objects/call.c 源码追踪一次 C 级调用的完整调用链。
一、两套调用协议总览
CPython 支持两种调用协议:
- tp_call 协议:通过
PyTypeObject的tp_call槽位实现,参数约定为"位置参数 tuple + 关键字参数 dict"; - vectorcall 协议:由 PEP 590 引入,参数直接以 C 数组形式传递,避免了 tuple/dict 的临时构建开销,目标是让调用更高效。
经验法则是:对内部调用,若被调对象支持 vectorcall,CPython 会优先使用 vectorcall;但这不是硬性规则(例如某些第三方扩展会直接使用 tp_call 而不经过 PyObject_Call),因此支持 vectorcall 的类必须同时实现语义一致的 tp_call。
二、tp_call 协议
设置了 PyTypeObject.tp_call 槽的类,其实例是可调用对象。该槽的函数签名为:
PyObject *tp_call(PyObject *callable, PyObject *args, PyObject *kwargs);
调用约定与 Python 代码中的 callable(*args, **kwargs) 一致:
args是位置参数组成的 tuple,不允许为 NULL(无参数时需传空 tuple);kwargs是关键字参数组成的 dict,无关键字参数时可以为 NULL;- 这一约定不仅被 tp_call 使用,
PyTypeObject.tp_new和PyTypeObject.tp_init也以相同方式接收参数。
调用对象时使用 PyObject_Call 或其他 Object Calling API 函数(见第五节)。
tp_call 的底层执行路径
在 Objects/call.c 中,_PyObject_MakeTpCall() 实现了 tp_call 的"慢路径":
/* Slow path: build a temporary tuple for positional arguments and a
* temporary dictionary for keyword arguments (if any) */
ternaryfunc call = Py_TYPE(callable)->tp_call;
if (call == NULL) {
object_is_not_callable(tstate, callable);
return NULL;
}
PyObject *argstuple = PyTuple_FromArray(args, nargs);
...
if (_Py_EnterRecursiveCallTstate(tstate, " while calling a Python object") == 0)
{
result = _PyCFunctionWithKeywords_TrampolineCall(
(PyCFunctionWithKeywords)call, callable, argstuple, kwdict);
_Py_LeaveRecursiveCallTstate(tstate);
}
从中可以确认文档的两点关键描述:
- 临时对象构建开销:慢路径需要
PyTuple_FromArray()把位置参数数组包装成临时 tuple,若关键字参数是 kwnames 形式还会经_PyStack_AsDict()构建临时 dict——这正是 vectorcall 想要省掉的步骤; - 递归保护自动完成:CPython 在调用前后自动包裹
_Py_EnterRecursiveCallTstate()/_Py_LeaveRecursiveCallTstate(),因此走 tp_call 的被调方无需自己担心递归深度。
另外,Objects/call.c 中的 object_is_not_callable() 还会给出贴心的错误提示:当误把模块当函数调用时(如 pprint(thing)),会提示 Did you mean: 'pprint.pprint(...)'?。
三、Vectorcall 协议(PEP 590)
Vectorcall 协议由 PEP 590 引入(3.9 年文档化,PyObject_Vectorcall 于 3.8 年以 _PyObject_Vectorcall 临时名出现),核心思想是用 C 数组直接传参,避免构建 tuple 与 dict。
3.1 为什么必须同时实现 tp_call
文档以醒目警告强调:
支持 vectorcall 的类必须同时实现语义一致的
PyTypeObject.tp_call。
原因是内部调用不保证总走 vectorcall:CPython 只在"被调对象支持 vectorcall"时优先选择它;同时仍有第三方扩展直接使用 tp_call。推荐做法是把 tp_call 槽直接指向 PyVectorcall_Call,这样两条路径行为天然一致。
3.12 起还有一个重要变化:当类的 __call__ 方法被重新赋值时,Py_TPFLAGS_HAVE_VECTORCALL 标志会被自动移除(因为这种赋值只修改 tp_call,可能导致两条路径行为不一致)。在 3.12 之前的版本中,vectorcall 应只用于不可变(Py_TPFLAGS_IMMUTABLETYPE)或静态类型。
另一个实用建议:如果实现 vectorcall 反而更慢(例如被调方无论如何都要把参数转回 args tuple 和 kwargs dict),就不该实现它。
3.2 启用方式与 vectorcallfunc 签名
类通过两步启用 vectorcall:
- 打开
Py_TPFLAGS_HAVE_VECTORCALL类型标志; - 把
PyTypeObject.tp_vectorcall_offset设置为对象结构体中vectorcallfunc指针字段的字节偏移。
函数指针类型为:
PyObject *(*vectorcallfunc)(PyObject *callable,
PyObject *const *args,
size_t nargsf,
PyObject *kwnames);
各参数含义:
| 参数 | 说明 |
|---|---|
callable |
被调用的对象本身 |
args |
C 数组:先是位置参数,再是关键字参数的值;无参数时可以为 NULL |
nargsf |
位置参数个数,可能叠加 PY_VECTORCALL_ARGUMENTS_OFFSET 标志位;用 PyVectorcall_NARGS() 提取真实个数 |
kwnames |
关键字参数名组成的 tuple(即 kwargs dict 的键),元素必须是 str 或其子类且互不重复;无关键字参数时可为 NULL |
注意一个关键细节:nargsf 只计算位置参数个数,不包含关键字参数。从 Include/internal/pycore_call.h 的内联函数 _PyObject_VectorcallTstate() 注释可见,关键字参数的值就存放在 args 数组中位置参数之后的位置,但不计入 nargsf。
3.3 PY_VECTORCALL_ARGUMENTS_OFFSET 标志
在 Include/abstract.h 中,该标志被定义为 size_t 的最高位:
#define PY_VECTORCALL_ARGUMENTS_OFFSET \
(_Py_STATIC_CAST(size_t, 1) << (8 * sizeof(size_t) - 1))
选择最高位的用意正是为了不与任何参数个数冲突(参数个数不可能触及符号位)。其语义分两种场景:
- 普通 vectorcall 调用:若
nargsf带此标志,被调方被允许临时修改args[-1]——即args实际指向参数 1(而非参数 0),调用方在数组前多预留了一个槽位。被调方返回前必须恢复args[-1]的原值。 PyObject_VectorcallMethod调用:此标志含义变为允许临时修改args[0](因为args[0]是方法所在的对象,见第五节)。
文档还给出了明确的使用建议:调用方在能廉价做到(不需要额外堆分配)时就应使用 PY_VECTORCALL_ARGUMENTS_OFFSET。这样做的直接收益是:像绑定方法这类需要在后续调用中前置插入 self 参数的可调用对象,可以零拷贝地完成续传。
这一点在 Objects/call.c 的 PyObject_VectorcallMethod() 实现中体现得非常清楚——它对三种情形分别处理:
if (self_obj == NULL) {
/* Skip "self". We can keep PY_VECTORCALL_ARGUMENTS_OFFSET since
* args[-1] in the onward call is args[0] here. */
result = _PyObject_VectorcallTstate(tstate, callable,
args + 1, nargsf - 1, kwnames);
}
else if (self_obj == args[0]) {
/* 去掉 OFFSET 标志,因为 args[-1] 现在不可被修改 */
result = _PyObject_VectorcallTstate(tstate, callable, args,
nargsf & ~PY_VECTORCALL_ARGUMENTS_OFFSET,
kwnames);
}
else {
/* classmethod:self_obj 是类型而非 args[0],需要 prepend 后调用 */
result = _PyObject_VectorcallPrepend(tstate, callable, self_obj,
args + 1, nargsf - 1, kwnames);
}
第三个分支用到的 _PyObject_VectorcallPrepend()(Objects/call.c)同样展示了标志位的双刃剑:当 PY_VECTORCALL_ARGUMENTS_OFFSET 已设置时,它可以直接把 args - 1 处的槽位改写为 arg 再恢复,完全避免拷贝;否则需要借助栈上小数组或 PyMem_Malloc 复制整个参数向量。
3.4 调用与快慢路径选择
调用支持 vectorcall 的对象,与普通可调用对象一样使用 Object Calling API 即可;PyObject_Vectorcall 通常是效率最高的选择。
其内部实现 _PyObject_VectorcallTstate()(Include/internal/pycore_call.h)清晰地展示了"优先 vectorcall、回退 tp_call"的选择逻辑:
static inline PyObject *
_ObjectO_VectorcallTstate(PyThreadState *tstate, PyObject *callable,
PyObject *const *args, size_t nargsf,
PyObject *kwnames)
{
vectorcallfunc func = _PyVectorcall_FunctionInline(callable);
if (func == NULL) {
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
return _PyObject_MakeTpCall(tstate, callable, args, nargs, kwnames);
}
res = func(callable, args, nargsf, kwnames);
return _Py_CheckFunctionResult(tstate, callable, res, NULL);
}
而 _PyVectorcall_FunctionInline()(Include/internal/pycore_call.h)的查找方式是:先检查类型是否带 Py_TPFLAGS_HAVE_VECTORCALL 标志,再用 memcpy 按 tp_vectorcall_offset 从对象内存中取出函数指针——这解释了为何 offset 是对象结构体中的字节偏移而非函数表索引。
四、递归控制:两套协议的关键差异
这是编写 vectorcall 实现时最容易踩的坑:
- tp_call 调用:被调方不需要关心递归,CPython 已通过
Py_EnterRecursiveCall/Py_LeaveRecursiveCall包裹(参见上文_PyObject_MakeTpCall()源码); - vectorcall 调用:出于性能考虑,CPython 不会替你检查递归深度,被调方如有需要必须自己调用
Py_EnterRecursiveCall/Py_LeaveRecursiveCall。
五、Vectorcall 支持 API
5.1 PyVectorcall_NARGS
Py_ssize_t PyVectorcall_NARGS(size_t nargsf);
从 nargsf 中提取真实位置参数个数。当前实现等价于:
(Py_ssize_t)(nargsf & ~PY_VECTORCALL_ARGUMENTS_OFFSET)
在 Include/cpython/abstract.h 中它以静态内联函数 _PyVectorcall_NARGS() 提供;而在 Objects/call.c 末尾,它又被 #undef 后重新导出为函数——因为 stable ABI 需要它作为导出符号。文档提醒:应始终使用 PyVectorcall_NARGS 而非手写掩码运算,以便未来扩展。
5.2 PyVectorcall_Function
vectorcallfunc PyVectorcall_Function(PyObject *op);
若 op 不支持 vectorcall(类型不支持或该实例不支持)返回 NULL,否则返回存放在 op 中的 vectorcall 函数指针;该函数永不抛异常。最常用的用途是能力探测:
if (PyVectorcall_Function(op) != NULL) {
/* op 支持 vectorcall */
}
其内部实现 PyVectorcall_Function()(Objects/call.c)只是一行转发到 _PyVectorcall_FunctionInline()。
5.3 PyVectorcall_Call
PyObject *PyVectorcall_Call(PyObject *callable, PyObject *tuple, PyObject *dict);
用 tuple + dict 形式的参数直接调用 callable 的 vectorcallfunc。这是一个专用函数,设计目的是放进 tp_call 槽位或供 tp_call 的实现使用。两个重要限制(Objects/call.c):
- 它不检查
Py_TPFLAGS_HAVE_VECTORCALL标志; - 它不会回退到
tp_call;对象若不支持 vectorcall(offset ≤ 0 或槽位函数指针为 NULL),直接抛TypeError: '...' object does not support vectorcall。
其快慢路径逻辑在 _PyVectorcall_Call()(Objects/call.c)中:无关键字参数时直接复用 tuple 的内部指针数组 func(callable, _PyTuple_ITEMS(tuple), nargs, NULL),零分配;有关键字参数时经 _PyStack_UnpackDict() 展开并带上 PY_VECTORCALL_ARGUMENTS_OFFSET 调用后释放。
六、Object Calling API 函数族
CPython 提供了一系列调用函数,每个函数都把参数转换成被调对象支持的约定(tp_call 或 vectorcall)。选型原则文档说得很直白:用哪种取决于你手头数据的形态,尽量做最少的转换。
6.1 函数速查表
| 函数 | callable | args | kwargs |
|---|---|---|---|
PyObject_Call |
PyObject * |
tuple | dict/NULL |
PyObject_CallNoArgs |
PyObject * |
— | — |
PyObject_CallOneArg |
PyObject * |
1 个对象 | — |
PyObject_CallObject |
PyObject * |
tuple/NULL |
— |
PyObject_CallFunction |
PyObject * |
格式串 | — |
PyObject_CallMethod |
对象 + char * |
格式串 | — |
PyObject_CallFunctionObjArgs |
PyObject * |
可变参 PyObject * |
— |
PyObject_CallMethodObjArgs |
对象 + 名称对象 | 可变参 PyObject * |
— |
PyObject_CallMethodNoArgs |
对象 + 名称对象 | — | — |
PyObject_CallMethodOneArg |
对象 + 名称对象 | 1 个对象 | — |
PyObject_Vectorcall |
PyObject * |
vectorcall 约定 | vectorcall 约定 |
PyObject_VectorcallDict |
PyObject * |
vectorcall 约定 | dict/NULL |
PyObject_VectorcallMethod |
名称 + args[0] |
vectorcall 约定 | vectorcall 约定 |
6.2 基础调用函数
PyObject_Call(PyObject *callable, PyObject *args, PyObject *kwargs) —— 等价于 callable(*args, **kwargs)。args 必须是 tuple 且不得为 NULL(无参数传空 tuple);kwargs 无时可为 NULL。实现 _PyObject_Call()(Objects/call.c)先尝试 vectorcall 快路径,失败才回退 tp_call。
PyObject_CallNoArgs(PyObject *callable)(3.9+)—— 无参调用,是"无参数调用可调用对象"的最高效方式。实现极简(Objects/call.c):
PyObject *
PyObject_CallNoArgs(PyObject *func)
{
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, func);
PyThreadState *tstate = _PyThreadState_GET();
return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL);
}
PyObject_CallOneArg(PyObject *callable, PyObject *arg)(3.9+)—— 恰好 1 个位置参数。其实现展示了 PY_VECTORCALL_ARGUMENTS_OFFSET 的教科书式用法(Objects/call.c):
PyObject *_args[2];
PyObject **args = _args + 1; // For PY_VECTORCALL_ARGUMENTS_OFFSET
args[0] = arg;
size_t nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET;
return _PyObject_VectorcallTstate(tstate, func, args, nargsf, NULL);
只分配了栈上 2 个指针的数组(第一个槽留给被调方可能的改写),不触碰堆。
PyObject_CallObject(PyObject *callable, PyObject *args) —— 等价于 callable(*args);无参数时 args 可为 NULL,此时内部直接走无参快路径(Objects/call.c)。
PyObject_CallFunction(PyObject *callable, const char *format, ...) —— 用 Py_BuildValue 风格的格式串描述变参 C 参数;format 可为 NULL 表示无参数。等价于 callable(*args)。注意文档提示:若参数都是现成的 PyObject *,用 PyObject_CallFunctionObjArgs 更快。实现 _PyObject_CallFunctionVa()(Objects/call.c)中还有个历史兼容细节:PyObject_CallFunction(func, "O", tuple) 会退化为 func(*tuple)。
PyObject_CallMethod(PyObject *obj, const char *name, const char *format, ...) —— 调用 obj 上名为 name 的方法,格式串同样需产出 tuple,等价于 obj.name(arg1, arg2, ...)。从源码看(Objects/call.c)其做法是 PyObject_GetAttrString() 取属性后复用 _PyObject_CallFunctionVa(),并先经 PyCallable_Check() 校验可调用性。
PyObject_CallFunctionObjArgs(PyObject *callable, ...) —— 传变长 PyObject * 参数、以 NULL 结尾,等价于 callable(arg1, arg2, ...)。
PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ...) —— 方法名以 Python 字符串对象给出,参数同样以 NULL 结尾的 PyObject * 变参列表给出。实现走 _PyObject_GetMethodStackRef() + object_vacall()(Objects/call.c)。
PyObject_CallMethodNoArgs(PyObject *obj, PyObject *name) / PyObject_CallMethodOneArg(PyObject *obj, PyObject *name, PyObject *arg)(均 3.9+)—— 分别是无参方法与单参方法调用。
这些函数在成功时返回结果,失败时置异常并返回 NULL,调用方必须检查。
6.3 Vectorcall 系列
PyObject_Vectorcall(PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwnames) —— 参数约定与 vectorcallfunc 完全相同;若被调对象支持 vectorcall 则直接调用其 vectorcall 函数,否则回退 tp_call。3.8 年以临时名 _PyObject_Vectorcall 出现,3.9 年更名(旧名已被软弃用)。
PyObject_VectorcallDict(PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwdict)(3.9+)—— 位置参数用 vectorcall 约定(args 数组只含位置参数),关键字参数则以 dict 传入。由于无论内部走哪条协议都要做一次参数转换,文档给出明确使用场景:当你手头已有一个现成的 kwargs dict、却没有位置参数 tuple 时才用它。实现 _PyObject_VectorcallDictTstate()(Objects/call.c)中,若被调方支持 vectorcall,会经 _PyStack_UnpackDict() 把 dict 展开成 kwnames 形式后再调用;否则直接交给 _PyObject_MakeTpCall()。
PyObject_VectorcallMethod(PyObject *name, PyObject *const *args, size_t nargsf, PyObject *kwnames)(3.9+)—— 用 vectorcall 约定调用方法:name 是方法名的 Python 字符串,被调对象是 args[0],args[1] 起才是真正的调用参数,至少要有 1 个位置参数;nargsf 包含 args[0],若允许临时改写 args[0] 则叠加 PY_VECTORCALL_ARGUMENTS_OFFSET。若对象具备 Py_TPFLAGS_METHOD_DESCRIPTOR 特性,则以完整 args 向量调用未绑定方法对象。
6.4 类型系统对 vectorcall 的继承规则
除了文档正文,Objects/typeobject.c 还揭示了子类型创建时的继承规则,可帮助理解标志的生命周期:
/* Always inherit tp_vectorcall_offset to support PyVectorcall_Call().
* If Py_TPFLAGS_HAVE_VECTORCALL is not inherited, then vectorcall
* won't be used automatically. */
COPYSLOT(tp_vectorcall_offset);
/* Inherit Py_TPFLAGS_HAVE_VECTORCALL if tp_call is not overridden */
if (!type->tp_call &&
_PyType_HasFeature(base, Py_TPFLAGS_HAVE_VECTORCALL))
{
type_add_flags(type, Py_TPFLAGS_HAVE_VECTORCALL);
}
COPYSLOT(tp_call);
即:tp_vectorcall_offset 总是从基类继承(保证 PyVectorcall_Call() 始终可用),但 Py_TPFLAGS_HAVE_VECTORCALL 只在子类未覆写 tp_call 时才继承——这与 3.12 版本变化(覆写 __call__ 即摘除标志)在语义上完全呼应。
七、Call Support API:PyCallable_Check
int PyCallable_Check(PyObject *o);
判断对象是否可调用:可调用返回 1,否则返回 0,永远成功。从 Objects/object.c 看,它的判据只有一条:
int
PyCallable_Check(PyObject *x)
{
if (x == NULL)
return 0;
return Py_TYPE(x)->tp_call != NULL;
}
即检查类型是否有 tp_call 槽——这也从侧面印证了文档开头那句要求:支持 vectorcall 的类必须实现 tp_call,否则连 PyCallable_Check 都不会认它是可调用对象。
八、实践要点小结
- 扩展作者实现可调用类型:
tp_call必填(PyCallable_Check依赖它),tp_call指向PyVectorcall_Call、tp_vectorcall_offset指向实例中的vectorcallfunc字段、类型开启Py_TPFLAGS_HAVE_VECTORCALL,是推荐的最小一致组合; - vectorcall 被调方需自行管理
Py_EnterRecursiveCall/Py_LeaveRecursiveCall,且返回前必须恢复被改写的前置槽位(args[-1]或args[0]); - 调用方选型:无参数用
PyObject_CallNoArgs;单参数用PyObject_CallOneArg;现成PyObject *列表用...ObjArgs系列;格式串参数用PyObject_CallFunction/CallMethod;已持有 vectorcall 形态数据时用PyObject_Vectorcall系列,且尽量在零分配可达时带上PY_VECTORCALL_ARGUMENTS_OFFSET; - 版本前提:vectorcall 相关公共 API 需要 3.9+(
PY_VECTORCALL_ARGUMENTS_OFFSET自 3.8 引入,PyObject_Vectorcall3.8 时为_PyObject_Vectorcall临时名);PyObject_Vectorcall/PyObject_VectorcallMethod在 limited API 下需要Py_LIMITED_API >= 0x030C0000(见 Include/abstract.h 的条件编译)。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00