首页
/ CPython C-API 方法对象深度解析:PyMethod_New、绑定方法与 Instance Method 类型

CPython C-API 方法对象深度解析:PyMethod_New、绑定方法与 Instance Method 类型

2026-09-06 23:05:11作者:滕妙奇

本文围绕 CPython 官方 C-API 文档 Doc/c-api/method.rst 中定义的"方法对象"体系展开,系统讲解两类对象——Instance Method(实例方法)与 Method(绑定方法)——的 API、类型对象与底层实现,并结合 Objects/classobject.cInclude/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_methodPyCFunctionObject)属于另一套类型,其声明头文件 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;
}

从源码结构看有两个值得注意的实现细节:

  1. 引用语义Py_NewRef(func) 表明新对象持有 func 的独立引用,入参的引用计数不受影响;
  2. 无 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) 生成一个真正的绑定方法对象;从类上直接访问(objNULL)则原样返回内部函数。而直接调用实例方法本身时,instancemethod_call 只是把调用原样转发给 PyObject_Call(PyInstanceMethod_GET_FUNCTION(self), arg, kw)

对应的类型对象 PyInstanceMethod_Typetp_name = "instancemethod",并注册了 tp_descr_gettp_calltp_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_funcim_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;
}

从源码看有四层工程考量:

  1. 防御性检查self == NULL 时设置 PyErr_BadInternalCall 并返回 NULL——对应文档"self must not be NULL"的硬性约束;
  2. freelist 快路径:优先从 per-interpreter 的 freelist(pymethodobjects)复用旧对象,miss 时才 PyObject_GC_New,与 meth_dealloc 中的 _Py_FREELIST_FREE 配对,因为绑定方法在 CPython 中创建/销毁频率极高;
  3. 引用语义im_funcim_self 均通过 Py_NewRef 持有独立引用,创建者无需担心传入对象的引用被吞掉;
  4. GC 追踪:对象含两个堆引用且参与循环检测(tp_flagsPy_TPFLAGS_HAVE_GC,并有 method_traverse 访问 im_funcim_self)。

调用链:self 是如何"前置"到参数表的

PyMethod_Typetp_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_Typetp_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) 时:第一个参数必须是可调用对象,第二个参数不允许是 NonePyMethod_NewNULL 的检查针对 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_funcPyObject_RichCompareBool,函数相等时再按指针身份a->im_self == b->im_self)比较实例;
  • reprmethod_repr 输出 <bound method {func.__qualname__} of {self!r}>,优先使用 __qualname__ 回退到 __name__

使用建议与工程实践要点

结合文档约束与源码行为,在 C 扩展中使用这些 API 时建议注意:

  1. PyMethod_Newself 不可为 NULL:C 层传入 NULL 会触发 PyErr_BadInternalCall(见 PyMethod_New);Python 层的 types.MethodType 则额外拒绝 None 实例;
  2. PyMethod_Function / PyMethod_Self 与 GET 宏的选择:带 _Function/_Self 后缀的函数会先做 PyMethod_Check,类型不符时调用 PyErr_BadInternalCall 并返回 NULLPyMethod_GET_FUNCTION/PyMethod_GET_SELF 宏则完全跳过检查,只应在已确认类型的热路径使用(Include/cpython/classobject.h#L34-L42)。从源码看这两个函数直接返回结构体成员指针、未做 Py_NewRef,即返回的是借用引用,若要将结果跨作用域持有需自行 Py_NewRef
  3. PyInstanceMethod_New 的 func 无调用性校验:文档只要求"any callable object",源码 PyInstanceMethod_New 也不做 PyCallable_Check;只有 Python 侧的 instancemethod.__new__instancemethod_new_impl)才会校验可调用性并抛出 TypeError——C 层调用者需自行保证;
  4. 区分 Method 与 CFunction 对象:绑定用户函数用本文的 PyMethod_New;而把 C 函数(PyMethodDef)暴露为 Python 方法时应使用 PyCFunction_NewEx/PyCMethod_New(见 Objects/methodobject.c),其中带 METH_METHOD 标志的 PyCMethod 类型额外保存了所属类引用并在调用时同时传入 self 与 class,是从 C 扩展内部构造"类绑定方法"的现代做法;
  5. 仓库中的真实用例:标准库 functools.partial 在支持绑定(实现 __get__)时就是直接调用 PyMethod_New(self, obj),参见 Modules/_functoolsmodule.c#L365Modules/_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 前置参数、属性回退与借用引用返回等手段,把这两个小对象打磨成了调用链路上性能敏感的关键环节。

关键参考文件

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