首页
/ CPython 描述符对象 C API 详解:从 PyDescr_New 系列函数到描述符协议实现

CPython 描述符对象 C API 详解:从 PyDescr_New 系列函数到描述符协议实现

2026-09-06 14:09:42作者:卓艾滢Kingsley

本文基于 CPython 官方 C API 文档 Doc/c-api/descriptor.rst,系统讲解 CPython 描述符对象(Descriptor Objects)的完整 C API:五类描述符创建函数(PyDescr_NewGetSetPyDescr_NewMemberPyDescr_NewMethodPyDescr_NewWrapperPyDescr_NewClassMethod)、对应的描述符类型对象、PyDescr_IsDataPyWrapper_New 工具函数,以及内置描述符类型(propertysuperclassmethodstaticmethod 的 C 层入口)。读完本文,你不仅能掌握这些 API 的签名、返回值约定与使用场景,还能对照 CPython 源码理解描述符如何进入类型字典、tp_descr_get/tp_descr_set 协议如何被调用,以及 PyDescr_Common 宏为何被标记为软弃用。

什么是描述符:对象类型字典中的“属性描述者”

按照 Doc/c-api/descriptor.rst 的定义:“Descriptors are objects that describe some attribute of an object. They are found in the dictionary of type objects.”——描述符是描述某个对象的部分属性的对象,它们存放在类型对象的字典(type.__dict__)中,而不是实例字典里。

这解释了 Python 层的日常现象:

>>> type(str.split)          # 方法描述符
<class 'methoddescriptor'>
>>> type(str.__dict__)        # 注意这是 mappingproxy,同文件也实现了它
<class 'mappingproxy'>
>>> str.split
<built-in method split of type object>

当你通过类访问 str.split 时,拿到的是描述符本身;通过实例访问时,CPython 的属性查找机制发现该描述符实现了 tp_descr_get 槽,就会调用它生成一个绑定对象(如 PyCFunction 或 wrapper 对象)。这一“数据描述符优先于实例字典、非数据描述符可被实例字典遮蔽”的规则,正是由本文介绍的 PyDescr_IsData 判断依据所支撑的。

CPython 在 C 层定义了五种主要描述符类型,每种类型都有对应的类型对象(PyTypeObject),并与 Python 层 types 模块中的类一一对应:

C API 类型对象 对应 Python 类型 用途
PyGetSetDescr_Type types.GetSetDescriptorType PyGetSetDef 创建的 getter/setter 描述符
PyMemberDescr_Type types.MemberDescriptorType PyMemberDef 创建的 C 结构体字段描述符
PyMethodDescr_Type types.MethodDescriptorType PyMethodDef 创建的方法描述符
PyWrapperDescr_Type types.WrapperDescriptorType 暴露类型槽(slot)实现的特殊方法,如 __repr____add__
PyClassMethodDescr_Type types.ClassMethodDescriptorType METH_CLASS 方法创建,绑定到类而非实例

这些类型对象均在 Include/descrobject.h 中通过 PyAPI_DATA(PyTypeObject) 声明(第 19–26 行),实现在 Objects/descrobject.c

描述符创建函数:API 全览

文档共给出五个 PyDescr_New* 创建函数,签名在 Include/descrobject.h(第 27–33 行)中声明,全部实现于 Objects/descrobject.c。它们的统一约定是:成功时返回描述符的强引用(strong reference),失败时返回 NULL 并设置异常

PyDescr_NewGetSet:C 级 getter/setter 描述符

PyObject* PyDescr_NewGetSet(PyTypeObject *type, struct PyGetSetDef *getset);

为扩展类型 typePyGetSetDef 结构 getset 创建一个 get-set 描述符。get-set 描述符暴露的属性不是直接存储在实例中,而是由 C 级 getter 和 setter 函数实现——这与 PyTypeObject.tp_getset 数组条目自动创建的描述符是同一类,在 Python 中呈现为 types.GetSetDescriptorType 对象。

PyGetSetDef 结构定义见 Include/descrobject.h(第 11–17 行):

struct PyGetSetDef {
    const char *name;       // 属性名
    getter get;            // PyObject *(*)(PyObject *obj, void *closure)
    setter set;            // int (*)(PyObject *obj, PyObject *value, void *closure)
    const char *doc;       // 文档字符串
    void *closure;         // 传给 get/set 的附加上下文
};

一个典型的 C 扩展用法是:在类型定义的 tp_getset 中列出 PyGetSetDef 数组,CPython 会在类型初始化时自动为每条记录创建描述符(见下文“描述符如何进入类型字典”)。而 PyDescr_NewGetSet 允许你在运行时手动创建,例如把某个已有类型的计算属性“搬运”到新类型中。

从源码看,PyDescr_NewGetSet 的实现极其精简(Objects/descrobject.c 第 1010–1020 行):调用统一的 descr_new 构造公共部分,再把 d_getset 指针指向传入的 PyGetSetDef 记录。注意描述符只保存指针而非拷贝,因此 PyGetSetDef 数组必须比描述符生命周期更长(扩展模块的静态数组天然满足)。

PyDescr_NewMember:C 结构体字段描述符

PyObject* PyDescr_NewMember(PyTypeObject *type, struct PyMemberDef *member);

为扩展类型 typePyMemberDef 结构 member 创建成员描述符。成员描述符把类型 C 结构体中的字段直接暴露为 Python 属性——这是 tp_members 条目创建的那类描述符,在 Python 中呈现为 types.MemberDescriptorType

PyMemberDef 与相关常量定义在 Include/descrobject.h(第 41–86 行),要点包括:

  • type 字段取值 Py_T_SHORTPy_T_INTPy_T_LONGPy_T_DOUBLEPy_T_STRINGPy_T_CHARPy_T_OBJECT_EXPy_T_PYSSIZET 等,决定如何把结构体内存解释为 Python 对象;
  • flags 支持 Py_READONLY(只读,无法通过 Python 赋值)、Py_AUDIT_READ(读取时触发 object.__getattr__ 审计事件,3.10 引入)与 Py_RELATIVE_OFFSET(内部相对偏移,不能用于本 API);
  • 数组必须以 name == NULL 的条目结尾。

实现上(Objects/descrobject.c 第 992–1008 行),PyDescr_NewMember 会显式拒绝 Py_RELATIVE_OFFSET 标志并抛出 SystemError

if (member->flags & Py_RELATIVE_OFFSET) {
    PyErr_SetString(PyExc_SystemError,
        "PyDescr_NewMember used with Py_RELATIVE_OFFSET");
    return NULL;
}

这与 tp_members 的自动处理不同:相对偏移只能由类型内部的成员填充逻辑解析,手动创建的描述符必须以绝对偏移为准。

PyDescr_NewMethod 与 PyDescr_NewClassMethod:方法描述符

PyObject* PyDescr_NewMethod(PyTypeObject *type, struct PyMethodDef *meth);
PyObject* PyDescr_NewClassMethod(PyTypeObject *type, PyMethodDef *method);
  • PyDescr_NewMethodtypePyMethodDef 创建方法描述符,把 C 函数暴露为类型上的方法。这是 tp_methods 条目创建的那类描述符,在 Python 中呈现为 types.MethodDescriptorType
  • PyDescr_NewClassMethod 创建类方法描述符,对应 tp_methods 中带有 METH_CLASS 标志的条目。类方法描述符在访问时绑定的是而不是实例,呈现为 types.ClassMethodDescriptorType

PyDescr_NewMethod 的实现(Objects/descrobject.c 第 934–978 行)有一个值得注意的细节:它根据 ml_flags 中的调用约定(METH_VARARGSMETH_FASTCALLMETH_NOARGSMETH_OMETH_METHOD 等组合)预计算并缓存一个 vectorcall 函数

switch (method->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS |
                            METH_O | METH_KEYWORDS | METH_METHOD))
{
    case METH_VARARGS:
        vectorcall = method_vectorcall_VARARGS;
        break;
    ...
    default:
        PyErr_Format(PyExc_SystemError,
                     "%s() method: bad call flags", method->ml_name);
        return NULL;
}

也就是说,创建阶段就确定了绑定的 PyCFunction 之后的向量调用路径;flags 组合非法时直接以 SystemError 失败。而 PyDescr_NewClassMethod(第 980–990 行)不缓存 vectorcall,仅记录 d_method 指针。

PyDescr_NewWrapper 与 wrapperbase:类型槽的特殊方法描述符

PyObject* PyDescr_NewWrapper(PyTypeObject *type, struct wrapperbase *base, void *wrapped);

typewrapperbase 结构 base 与被包装的槽函数指针 wrapped 创建 wrapper 描述符。wrapper 描述符暴露由类型槽实现的特殊方法——正是 CPython 为 __repr____add__ 这类槽式特殊方法创建的那类描述符,在 Python 中呈现为 types.WrapperDescriptorType

wrapperbase 结构定义在 Include/cpython/descrobject.h(第 11–19 行,属于内部头文件,不暴露给受限 API):

struct wrapperbase {
    const char *name;        // Python 可见名称,如 "__repr__"
    int offset;              // 在类型中的槽偏移
    void *function;
    wrapperfunc wrapper;     // 把槽适配到 Python 调用约定的包装函数
    const char *doc;
    int flags;               // PyWrapperFlag_KEYWORDS(1) 表示 wrapper 接收关键字参数
    PyObject *name_strobj;
};

PyDescr_NewWrapper 的实现(Objects/descrobject.c 第 1022–1034 行)同样保存 d_based_wrapped 两个指针。该函数在 Py_LIMITED_API 之外的 C API 中可用,但 wrapperbase 结构本身来自内部头,实际使用场景主要是 CPython 内部或深度嵌入定制。

PyDescr_IsData:区分数据描述符与非数据描述符

int PyDescr_IsData(PyObject *descr);

返回非零当且仅当 descr 描述的是一个数据属性(data attribute),否则(描述方法)返回 0。文档明确强调:descr 必须是描述符对象,不做错误检查

实现只有一行(Objects/descrobject.c 第 1036–1040 行):

int
PyDescr_IsData(PyObject *ob)
{
    return Py_TYPE(ob)->tp_descr_set != NULL;
}

从源码结构看,判定标准就是描述符类型是否实现了 tp_descr_set 槽:能“写”的描述符(如成员描述符、get-set 描述符、property)是数据描述符,只有 tp_descr_get 的方法描述符则是非数据描述符。这直接决定了属性查找的优先级——数据描述符会遮蔽实例字典中的同名键,非数据描述符则不会。

PyWrapper_New:绑定 wrapper 对象

PyObject* PyWrapper_New(PyObject *d, PyObject *self);

由 wrapper 描述符 d 与实例 self 创建新的绑定 wrapper 对象。这是 PyDescr_NewWrapper 创建的描述符的绑定形式:当通过实例访问一个 slot wrapper 时,CPython 就会创建这类对象,它在 Python 中呈现为 types.MethodWrapperType(例如 (1, 2).__add__)。

实现(Objects/descrobject.c 第 1509–1527 行)包含两条 assert 前置条件:d 必须是 PyWrapperDescr_Type 类型的描述符,且 self 的类必须是描述符所属类型的子类。函数为 wrapperobject 分配 GC 追踪对象并强引用保存 descrself 两个字段。

描述符的内部结构:PyDescr_COMMON 与软弃用

所有描述符共享一个公共前缀结构 PyDescrObject,定义在 Include/cpython/descrobject.h(第 26–36 行):

typedef struct {
    PyObject_HEAD
    PyTypeObject *d_type;   // 描述符所属的类型
    PyObject *d_name;       // 属性名(interned 字符串)
    PyObject *d_qualname;   // 限定名
} PyDescrObject;

#define PyDescr_COMMON PyDescrObject d_common
#define PyDescr_TYPE(x)  (((PyDescrObject *)(x))->d_type)
#define PyDescr_NAME(x)  (((PyDescrObject *)(x))->d_name)

各具体描述符类型只是在此基础上追加自己的指针字段:

typedef struct { PyDescr_COMMON; PyMethodDef *d_method; vectorcallfunc vectorcall; } PyMethodDescrObject;
typedef struct { PyDescr_COMMON; PyMemberDef *d_member; } PyMemberDescrObject;
typedef struct { PyDescr_COMMON; PyGetSetDef *d_getset; } PyGetSetDescrObject;
typedef struct { PyDescr_COMMON; struct wrapperbase *d_base; void *d_wrapped; } PyWrapperDescrObject;

Doc/c-api/descriptor.rstPyDescr_COMMON 宏给出了重要告诫:

This was included in Python's C API by mistake; do not use it in extensions.

该宏被错误地纳入了 Python C API,文档标注其于 3.15 起软弃用(soft-deprecated)。如果你在编写自定义描述符类型,文档建议的正确做法是:定义一个自己的类,实现描述符协议,即设置 PyTypeObjecttp_descr_gettp_descr_set 两个槽,而不是套用 PyDescr_COMMON 布局。

描述符如何进入类型字典:CPython 的内部流程

CPython 在类型初始化时遍历 tp_methodstp_memberstp_getset 数组,为每条记录创建描述符并写入类型字典。这一过程在 Objects/typeobject.c 中:

  • type_add_members(第 8576–8597 行):遍历 type->tp_members,对每条记录调用 PyDescr_NewMember(type, memb),再用 PyDict_SetDefaultRefPyDescr_NAME(descr) 为名存入类型字典——SetDefault 语义意味着同名的 Python 层覆盖不会被 C 层记录冲掉;
  • type_add_getset(第 8600–8622 行):同样的模式调用 PyDescr_NewGetSet(type, gsp)
  • type_add_method(约第 8480–8549 行):按 ml_flags 分派——METH_CLASSPyDescr_NewClassMethodMETH_STATICPyStaticMethod_New(注意它不是 PyDescrObject 派生类型),其余走 PyDescr_NewMethod

所有创建路径最终汇聚到统一的工厂函数 descr_newObjects/descrobject.c 第 914–932 行):

static PyDescrObject *
descr_new(PyTypeObject *descrtype, PyTypeObject *type, const char *name)
{
    PyDescrObject *descr;
    descr = (PyDescrObject *)PyType_GenericAlloc(descrtype, 0);
    if (descr != NULL) {
        _PyObject_SetDeferredRefcount((PyObject *)descr);
        descr->d_type = (PyTypeObject*)Py_XNewRef(type);   // 强引用所属类型
        descr->d_name = PyUnicode_InternFromString(name);  // 名称被 intern
        ...
    }
    return descr;
}

两个值得注意的实现事实:其一,d_name 通过 PyUnicode_InternFromString 国际化,保证同一属性名全进程共享一个字符串对象,这也是 PyDescr_NAME(descr) 能安全用作字典键的原因;其二,descr->d_type 持有所属类型的强引用,描述符析构时由 descr_dealloc(第 22–31 行)释放。

描述符协议:tp_descr_get / tp_descr_set 的调用链

描述符的“魔法”发生在属性访问时。以成员描述符为例,其 tp_descr_get 实现 member_getObjects/descrobject.c 第 162–181 行)展示了完整协议流程:

  1. obj == NULL(通过类访问):返回描述符自身的新引用(Py_NewRef(descr))——这就是 Type.attr 返回描述符本身的原因;
  2. descr_check:校验实例是否属于 d_type,不匹配则抛出形如 descriptor 'x' for 'Y' objects doesn't apply to a 'Z' objectTypeError
  3. PyMemberDef.flagsPy_AUDIT_READ,先触发 PySys_Audit("object.__getattr__", ...) 审计;
  4. 调用 PyMember_GetOne((char *)obj, descr->d_member) 按类型码从结构体内存读出 Python 对象。

get-set 描述符的 getset_get(第 183–201 行)与 getset_set(第 242–259 行)遵循同样骨架:类访问返回自身;实例访问时先做 descr_check(写入路径对应 descr_setcheck),然后调用 d_getset->get/d_getset->set,并传入 closure;若 getter/setter 为 NULL 则抛出 “not readable”/“not writable” 的 AttributeError。源码中这些调用经由 descr_get_trampoline_call/descr_set_trampoline_call 转发,以支持 WebAssembly 等平台的调用约定适配。

方法描述符的 method_get(第 137–160 行)则演示了“绑定”的诞生:实例访问时根据 METH_METHOD 标志选择创建 PyCMethod(支持向量调用与 class 传递)或经典的 PyCFunction_NewEx(descr->d_method, obj, NULL)。这就是为什么 instance.split 是 bound method,而 str.split 是描述符。

内置描述符类型:property、super、classmethod、staticmethod

Doc/c-api/descriptor.rst 的 “Built-in descriptors” 一节列出了 C 层可直接使用的内置描述符类型对象:

C API 符号 Python 层对应 说明
PyProperty_Type property property 对象的类型对象,两个符号是同一个对象
PySuper_Type super super 对象的类型对象,同为同一对象
PyClassMethod_Type classmethod classmethod 对象的类型
PyClassMethodDescr_Type types.ClassMethodDescriptorType C 层类方法描述符类型,对应 C 扩展类型中定义 classmethod 时创建的描述符
PyStaticMethod_Type staticmethod staticmethod 对象的类型

配套的两个构造函数:

PyObject *PyClassMethod_New(PyObject *callable);
PyObject *PyStaticMethod_New(PyObject *callable);

PyClassMethod_New 创建包装 callable 的新 classmethod 对象;PyStaticMethod_New 创建包装 callable 的新 staticmethod 对象。两者都要求 callable 必须是可调用对象且不得为 NULL;成功时返回新对象的强引用,失败返回 NULL 并设置异常。

值得指出的是 property 本身就是描述符协议的 C 级范本。Objects/descrobject.c 第 1530 行起(注释中的等价 Python 代码从第 1534 行开始)给出了 propertyobject 的完整语义:__get__inst is None 时返回自身(即类访问得到 property 对象本身),getter 缺失时抛 AttributeError("property has no getter")getter()/setter()/deleter() 辅助方法通过 property_copy 返回替换了相应回调的新 property 副本(第 1591–1608 行)。propertyfget/fset/fdel 属性正是用 PyMemberDefPy_READONLY 标志暴露的(第 1582–1588 行)——一个描述符类型内部又使用成员描述符的典型嵌套。

另外,Objects/descrobject.c 中还有 PyDictProxy_Type(第 24 行声明的 mappingproxy,即 Type.__dict__ 的只读代理类型),文件内第 1042 行起的注释也承认它“没有理由放在这个文件里,只是新增文件有点麻烦”——阅读该文件时可以把它视为随附的只读映射代理实现。

实践:在 C 扩展中定义描述符属性的完整示例

下面是一个最小 C 扩展片段,展示如何用 tp_memberstp_getset 数组声明描述符(无需手动调用 PyDescr_New* 函数,类型初始化会自动完成,见上文 Objects/typeobject.ctype_add_members/type_add_getset):

typedef struct {
    PyObject_HEAD
    int count;
} Counter;

static PyObject *
counter_get_total(PyObject *obj, void *closure)
{
    Counter *self = (Counter *)obj;
    return PyLong_FromLong(self->count * 2);  // 计算属性,不落存储
}

static int
counter_set_value(PyObject *obj, PyObject *value, void *closure)
{
    Counter *self = (Counter *)obj;
    if (PyFloat_Check(value)) {
        PyErr_SetString(PyExc_TypeError, "value must be int");
        return -1;
    }
    self->count = (int)PyLong_AsLong(value);
    return self->count == -1 && PyErr_Occurred() ? -1 : 0;
}

static PyMemberDef counter_members[] = {
    {"count", Py_T_INT, offsetof(Counter, count), Py_READONLY,
     "Access the raw counter value (read-only member descriptor)."},
    {NULL}
};

static PyGetSetDef counter_getsets[] = {
    {"total", counter_get_total, counter_set_value,
     "Computed attribute: twice the count.", NULL},
    {NULL}
};

行为验证:counter.counttypes.MemberDescriptorType(只读,实例字典无法遮蔽),counter.totaltypes.GetSetDescriptorType,读取调用 counter_get_total、写入调用 counter_set_value。若需要自定义的完全自定义描述符(例如只读、值来自全局状态的复杂属性),则应遵循文档建议,实现 tp_descr_get/tp_descr_set 槽,而不要使用已弃用的 PyDescr_COMMON 宏。

测试与验证路径

CPython 用 Lib/test/test_descr.py 覆盖描述符行为,其中包含对 types.MemberDescriptorType 的断言(如第 1471、1486 行)以及 TestGenericDescriptors 等测试类(第 6269 行),验证描述符协议在各种继承与代理场景下的表现。若你修改或依赖描述符行为,可直接运行该测试模块回归验证:

python -m test test_descr -v

小结

  • 五个创建函数、五种类型对象PyDescr_NewGetSet/PyMember/Method/ClassMethod/Wrapper 分别对应 PyGetSetDescr_Type/PyMemberDescr_Type/PyMethodDescr_Type/PyClassMethodDescr_Type/PyWrapperDescr_Type,成功返回强引用、失败返回 NULL 并置错;
  • 两个工具 APIPyDescr_IsData 通过 tp_descr_set != NULL 判定数据描述符(无错误检查);PyWrapper_New 生成 slot wrapper 的绑定形式(types.MethodWrapperType);
  • 内置描述符PyProperty_TypePySuper_TypePyClassMethod_TypePyStaticMethod_Type 与 Python 层的 property/super/classmethod/staticmethod 是同一对象,另可用 PyClassMethod_New/PyStaticMethod_New 在 C 层构造后两者;
  • 源码要点:统一工厂 descr_new 负责类型强引用与名称 intern;type_add_members/type_add_getset/type_add_method 是描述符进入类型字典的入口;属性访问经 tp_descr_get/tp_descr_set 协议分派到 member_getgetset_getmethod_get 等实现;
  • 注意事项PyDescr_COMMON 宏属于历史误入 C API 的部分,3.15 起软弃用,自定义描述符请直接实现 tp_descr_get/tp_descr_set 协议;PyDescr_NewMember 拒绝 Py_RELATIVE_OFFSETPyDescr_NewMethod 会在创建期校验调用约定 flags 并缓存 vectorcall 路径。
登录后查看全文
热门项目推荐
相关项目推荐