首页
/ CPython 调用协议完全指南:tp_call、Vectorcall(PEP 590)与 Object Calling API 深度解析

CPython 调用协议完全指南:tp_call、Vectorcall(PEP 590)与 Object Calling API 深度解析

2026-09-06 23:37:12作者:劳婵绚Shirley

CPython 的 C API 为扩展模块提供了两套"调用(call)"协议:经典的 tp_call 协议与 3.9 年引入的 vectorcall 协议(PEP 590),它们决定了 C 扩展如何触发 Python 对象、以及一次调用在底层会经过哪些参数转换。读完本文,你将掌握两套协议的工作机制、PY_VECTORCALL_ARGUMENTS_OFFSET 位标记的原理、完整的 Object Calling API 函数族(PyObject_CallPyObject_VectorcallMethod)的选型依据,并能结合 Objects/call.c 源码追踪一次 C 级调用的完整调用链。

一、两套调用协议总览

CPython 支持两种调用协议:

  • tp_call 协议:通过 PyTypeObjecttp_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_newPyTypeObject.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);
}

从中可以确认文档的两点关键描述:

  1. 临时对象构建开销:慢路径需要 PyTuple_FromArray() 把位置参数数组包装成临时 tuple,若关键字参数是 kwnames 形式还会经 _PyStack_AsDict() 构建临时 dict——这正是 vectorcall 想要省掉的步骤;
  2. 递归保护自动完成: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:

  1. 打开 Py_TPFLAGS_HAVE_VECTORCALL 类型标志;
  2. 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.cPyObject_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 标志,再用 memcpytp_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 形式的参数直接调用 callablevectorcallfunc。这是一个专用函数,设计目的是放进 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 都不会认它是可调用对象。

八、实践要点小结

  1. 扩展作者实现可调用类型tp_call 必填(PyCallable_Check 依赖它),tp_call 指向 PyVectorcall_Calltp_vectorcall_offset 指向实例中的 vectorcallfunc 字段、类型开启 Py_TPFLAGS_HAVE_VECTORCALL,是推荐的最小一致组合;
  2. vectorcall 被调方需自行管理 Py_EnterRecursiveCall/Py_LeaveRecursiveCall,且返回前必须恢复被改写的前置槽位(args[-1]args[0]);
  3. 调用方选型:无参数用 PyObject_CallNoArgs;单参数用 PyObject_CallOneArg;现成 PyObject * 列表用 ...ObjArgs 系列;格式串参数用 PyObject_CallFunction/CallMethod;已持有 vectorcall 形态数据时用 PyObject_Vectorcall 系列,且尽量在零分配可达时带上 PY_VECTORCALL_ARGUMENTS_OFFSET
  4. 版本前提:vectorcall 相关公共 API 需要 3.9+(PY_VECTORCALL_ARGUMENTS_OFFSET 自 3.8 引入,PyObject_Vectorcall 3.8 时为 _PyObject_Vectorcall 临时名);PyObject_Vectorcall/PyObject_VectorcallMethod 在 limited API 下需要 Py_LIMITED_API >= 0x030C0000(见 Include/abstract.h 的条件编译)。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389