CPython C-API 方法对象深度解析:PyMethod_New、绑定方法与 Instance Method 类型
本文围绕 CPython 官方 C-API 文档 Doc/c-api/method.rst 中定义的"方法对象"体系展开,系统讲解两类对象——Instance Method(实例方法)与 Method(绑定方法)——的 API、类型对象与底层实现,并结合 Objects/classobject.c 与 Include/cpython/classobject.h 源码,帮助你在编写 C 扩展时正确创建、检查并访问绑定方法对象,理解 types.MethodType 背后的实现机制。
两类"方法对象"的定位
CPython 中与方法相关的 C-API 对象有两类,二者解决的问题不同:
| 对象 | 类型对象 | 暴露给 Python | 作用 |
|---|---|---|---|
| Instance Method | PyInstanceMethod_Type |
否(文档明确说明) | 对 PyCFunction 的包装,将 C 函数绑定到类对象的"新方式" |
| Method(绑定方法) | PyMethod_Type |
是,即 types.MethodType |
将任意可调用对象绑定到某个实例上 |
原文档的核心表述如下:
- Instance Method:是对
PyCFunction的包装(wrapper),是"将PyCFunction绑定到类对象的新方式",它取代了旧式调用PyMethod_New(func, NULL, class); - Method(绑定方法对象):是"bound function objects(绑定的函数对象)",始终绑定到用户自定义类的某个实例上;"未绑定方法"(绑定到类对象的方法,即 Python 2 时代的 unbound method)已不再提供。
需要注意的一个易混淆点:与 Python 层的 types.MethodType 相对的 C 函数封装 builtin_function_or_method(PyCFunctionObject)属于另一套类型,其声明头文件 Include/methodobject.h 开头即注释:"This is about the type 'builtin_function_or_method', not Python methods in user-defined classes. See classobject.h for the latter."——也就是说,本文档讨论的 Method Objects 对应 Include/cpython/classobject.h,而不是 PyCFunction_Type。
Instance Method Objects:PyCFunction 与类之间的桥梁
类型对象与 API 清单
文档定义的 Instance Method 相关 API 共 4 个,加上类型对象 PyInstanceMethod_Type:
PyTypeObject PyInstanceMethod_Type; /* 文档:not exposed to Python programs */
int PyInstanceMethod_Check(PyObject *o);
PyObject * PyInstanceMethod_New(PyObject *func);
PyObject * PyInstanceMethod_Function(PyObject *im);
PyObject * PyInstanceMethod_GET_FUNCTION(PyObject *im);
PyInstanceMethod_Check(o):判断 o 是否具有PyInstanceMethod_Type类型,参数不得为NULL,该函数"总是成功"(不设置异常)。从 Include/cpython/classobject.h#L51 可见它实际是Py_IS_TYPE宏的别名,直接做类型指针比较;PyInstanceMethod_New(func):创建新的 instance method 对象,func 可以是任意可调用对象,即实例方法被调用时实际转发的函数;PyInstanceMethod_Function(im):返回与 instance method im 关联的函数对象(带类型检查);PyInstanceMethod_GET_FUNCTION(im):PyInstanceMethod_Function的宏版本,省去类型检查,调用者必须自行保证类型正确。
源码实现:极简结构
PyInstanceMethodObject 的结构体定义在 Include/cpython/classobject.h#L44-L47:
typedef struct {
PyObject_HEAD
PyObject *func;
} PyInstanceMethodObject;
整个结构只有一个数据域 func——它不像 Method 对象那样持有被绑定的实例,这正对应文档"包装 PyCFunction 并绑定到类"的定位。
创建函数 PyInstanceMethod_New 的实现非常直接:
PyObject *
PyInstanceMethod_New(PyObject *func) {
PyInstanceMethodObject *method;
method = PyObject_GC_New(PyInstanceMethodObject,
&PyInstanceMethod_Type);
if (method == NULL) return NULL;
method->func = Py_NewRef(func); /* 为 func 增加一个引用 */
_PyObject_GC_TRACK(method);
return (PyObject *)method;
}
从源码结构看有两个值得注意的实现细节:
- 引用语义:
Py_NewRef(func)表明新对象持有 func 的独立引用,入参的引用计数不受影响; - 无 freelist 缓存:与后文 Method 对象使用
_Py_FREELIST_POP快回收不同,instance method 直接走PyObject_GC_New,说明它在正常 Python 程序中几乎不会被显式创建(文档也说该类型"不暴露给 Python 程序"),属于低频对象。
描述符协议:取出时才真正"绑定"
Instance Method 类型实现了描述符协议 instancemethod_descr_get:
static PyObject *
instancemethod_descr_get(PyObject *descr, PyObject *obj, PyObject *type) {
PyObject *func = PyInstanceMethod_GET_FUNCTION(descr);
if (obj == NULL) {
return Py_NewRef(func);
}
else
return PyMethod_New(func, obj);
}
这正是文档"绑定 PyCFunction 到类"语义的运行期体现:当该对象放在类字典中、并从实例上访问时,CPython 会调用 PyMethod_New(func, obj) 生成一个真正的绑定方法对象;从类上直接访问(obj 为 NULL)则原样返回内部函数。而直接调用实例方法本身时,instancemethod_call 只是把调用原样转发给 PyObject_Call(PyInstanceMethod_GET_FUNCTION(self), arg, kw)。
对应的类型对象 PyInstanceMethod_Type 中 tp_name = "instancemethod",并注册了 tp_descr_get、tp_call、tp_members(仅 __func__ 只读成员)与 tp_getset(__doc__ 转发到内部函数)等槽位,与上述行为一一对应。
Method Objects:types.MethodType 的底层
API 清单
Method(绑定方法)对象是扩展开发者最常打交道的类型。文档列出的完整 API 为:
PyTypeObject PyMethod_Type; /* 暴露为 types.MethodType */
int PyMethod_Check(PyObject *o);
PyObject * PyMethod_New(PyObject *func, PyObject *self);
PyObject * PyMethod_Function(PyObject *meth);
PyObject * PyMethod_GET_FUNCTION(PyObject *meth);
PyObject * PyMethod_Self(PyObject *meth);
PyObject * PyMethod_GET_SELF(PyObject *meth);
语义要点(继承自原文档):
PyMethod_Check(o):判断是否为 method 对象(类型为PyMethod_Type),参数不得为NULL,总是成功;PyMethod_New(func, self):func 为任意可调用对象,self 为方法要绑定的实例,self不得为NULL;PyMethod_Function(meth)/PyMethod_Self(meth):分别返回方法关联的函数对象与绑定的实例;PyMethod_GET_FUNCTION(meth)/PyMethod_GET_SELF(meth):宏版本,不做类型检查,性能更好但用错类型会产生未定义行为。
结构体与创建路径
Include/cpython/classobject.h#L12-L18 定义了 PyMethodObject:
typedef struct {
PyObject_HEAD
PyObject *im_func; /* 实现该方法的函数(或其他可调用对象) */
PyObject *im_self; /* 方法所绑定的实例 */
PyObject *im_weakreflist; /* 弱引用列表 */
vectorcallfunc vectorcall;
} PyMethodObject;
im_func 与 im_self 两个数据域与 Python 层的只读属性 __func__、__self__ 一一对应(见 method_memberlist);vectorcall 槽位则让它支持 PEP 590 的快速调用协议。
PyMethod_New 的完整实现:
PyObject *
PyMethod_New(PyObject *func, PyObject *self)
{
if (self == NULL) {
PyErr_BadInternalCall();
return NULL;
}
PyMethodObject *im = _Py_FREELIST_POP(PyMethodObject, pymethodobjects);
if (im == NULL) {
im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
if (im == NULL) {
return NULL;
}
}
im->im_weakreflist = NULL;
im->im_func = Py_NewRef(func);
im->im_self = Py_NewRef(self);
im->vectorcall = method_vectorcall;
_PyObject_GC_TRACK(im);
return (PyObject *)im;
}
从源码看有四层工程考量:
- 防御性检查:
self == NULL时设置PyErr_BadInternalCall并返回NULL——对应文档"selfmust not beNULL"的硬性约束; - freelist 快路径:优先从 per-interpreter 的 freelist(
pymethodobjects)复用旧对象,miss 时才PyObject_GC_New,与 meth_dealloc 中的_Py_FREELIST_FREE配对,因为绑定方法在 CPython 中创建/销毁频率极高; - 引用语义:
im_func、im_self均通过Py_NewRef持有独立引用,创建者无需担心传入对象的引用被吞掉; - GC 追踪:对象含两个堆引用且参与循环检测(
tp_flags带Py_TPFLAGS_HAVE_GC,并有 method_traverse 访问im_func与im_self)。
调用链:self 是如何"前置"到参数表的
PyMethod_Type 把 tp_vectorcall_offset 指向结构体中的 vectorcall 字段,实际调用走 method_vectorcall:
static PyObject *
method_vectorcall(PyObject *method, PyObject *const *args,
size_t nargsf, PyObject *kwnames)
{
assert(Py_IS_TYPE(method, &PyMethod_Type));
PyThreadState *tstate = _PyThreadState_GET();
PyObject *self = PyMethod_GET_SELF(method);
PyObject *func = PyMethod_GET_FUNCTION(method);
return _PyObject_VectorcallPrepend(tstate, func, self, args, nargsf, kwnames);
}
_PyObject_VectorcallPrepend 会直接把 im_self 拼接在实参表最前面再调用 im_func,等价于 func(self, *args, **kwargs),且不经过 PyVectorcall_Call 的常规参数拷贝路径——这就是 obj.method() 比手动 method.__func__(obj, ...) 更快的底层原因。
从 Python 侧进入:types.MethodType
类型对象 PyMethod_Type 的 tp_new 槽位指向由 C-API Clinic 生成的 method_new,其核心校验逻辑在 method_new_impl:
static PyObject *
method_new_impl(PyTypeObject *type, PyObject *function, PyObject *instance)
{
if (!PyCallable_Check(function)) {
PyErr_SetString(PyExc_TypeError,
"first argument must be callable");
return NULL;
}
if (instance == NULL || instance == Py_None) {
PyErr_SetString(PyExc_TypeError,
"instance must not be None");
return None
}
return PyMethod_New(function, instance);
}
因此在 Python 层使用 types.MethodType(callable, instance) 时:第一个参数必须是可调用对象,第二个参数不允许是 None(PyMethod_New 对 NULL 的检查针对 C 层,而 None 在这里被显式拒绝)。类型文档字符串 "Create a bound instance method object." 直接取自 Clinic 注释。
属性转发、哈希与相等性
绑定方法在语义上"站在函数前面",源码中有几处体现:
- 属性查找:method_getattro 先在
method类型自身查找属性(__func__、__self__、__doc__等),找不到时回退到im_func上查找——源码注释引用了 Christian Tismer 的论点:方法属性应几乎总是覆盖函数属性,唯一例外是__doc__(method_get_doc 专门把__doc__转发到被绑定函数); - 哈希:method_hash 计算
hash(im_self) ^ hash(im_func),冲突值-1改写为-2; - 相等性:method_richcompare 仅支持
==/!=,先对im_func做PyObject_RichCompareBool,函数相等时再按指针身份(a->im_self == b->im_self)比较实例; - repr:method_repr 输出
<bound method {func.__qualname__} of {self!r}>,优先使用__qualname__回退到__name__。
使用建议与工程实践要点
结合文档约束与源码行为,在 C 扩展中使用这些 API 时建议注意:
PyMethod_New的self不可为NULL:C 层传入NULL会触发PyErr_BadInternalCall(见 PyMethod_New);Python 层的types.MethodType则额外拒绝None实例;PyMethod_Function/PyMethod_Self与 GET 宏的选择:带_Function/_Self后缀的函数会先做PyMethod_Check,类型不符时调用PyErr_BadInternalCall并返回NULL;PyMethod_GET_FUNCTION/PyMethod_GET_SELF宏则完全跳过检查,只应在已确认类型的热路径使用(Include/cpython/classobject.h#L34-L42)。从源码看这两个函数直接返回结构体成员指针、未做Py_NewRef,即返回的是借用引用,若要将结果跨作用域持有需自行Py_NewRef;PyInstanceMethod_New的 func 无调用性校验:文档只要求"any callable object",源码 PyInstanceMethod_New 也不做PyCallable_Check;只有 Python 侧的instancemethod.__new__(instancemethod_new_impl)才会校验可调用性并抛出TypeError——C 层调用者需自行保证;- 区分 Method 与 CFunction 对象:绑定用户函数用本文的
PyMethod_New;而把 C 函数(PyMethodDef)暴露为 Python 方法时应使用PyCFunction_NewEx/PyCMethod_New(见 Objects/methodobject.c),其中带METH_METHOD标志的PyCMethod类型额外保存了所属类引用并在调用时同时传入 self 与 class,是从 C 扩展内部构造"类绑定方法"的现代做法; - 仓库中的真实用例:标准库
functools.partial在支持绑定(实现__get__)时就是直接调用PyMethod_New(self, obj),参见 Modules/_functoolsmodule.c#L365 与 Modules/_functoolsmodule.c#L1759——即partial对象从实例上访问时会包装成一个绑定方法,这是上述 API 在解释器内部最典型的调用方之一。
小结
Doc/c-api/method.rst 虽篇幅精炼,却覆盖了 C 扩展处理"绑定"关系的完整工具箱:PyInstanceMethod_Type 一族 API 面向"C 函数 + 类"的低频场景,PyMethod_Type 一族则承载了 types.MethodType 与实例方法访问的高频路径。结合 Objects/classobject.c 的源码可以看到,CPython 通过 freelist 复用、vectorcall 前置参数、属性回退与借用引用返回等手段,把这两个小对象打磨成了调用链路上性能敏感的关键环节。
关键参考文件:
- 文档:Doc/c-api/method.rst
- 结构体与内部声明:Include/cpython/classobject.h
- 类型实现:Objects/classobject.c
- 对照参考(
builtin_function_or_method):Include/methodobject.h、Objects/methodobject.c - 内部调用示例:Modules/_functoolsmodule.c
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00