CPython 描述符对象 C API 详解:从 PyDescr_New 系列函数到描述符协议实现
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本文基于 CPython 官方 C API 文档 Doc/c-api/descriptor.rst,系统讲解 CPython 描述符对象(Descriptor Objects)的完整 C API:五类描述符创建函数(PyDescr_NewGetSet、PyDescr_NewMember、PyDescr_NewMethod、PyDescr_NewWrapper、PyDescr_NewClassMethod)、对应的描述符类型对象、PyDescr_IsData与PyWrapper_New工具函数,以及内置描述符类型(property、super、classmethod、staticmethod的 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);为扩展类型type从PyGetSetDef结构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);为扩展类型type从PyMemberDef结构member创建成员描述符。成员描述符把类型 C 结构体中的字段直接暴露为 Python 属性——这是tp_members条目创建的那类描述符,在 Python 中呈现为types.MemberDescriptorType。
PyMemberDef与相关常量定义在 Include/descrobject.h(第 41–86 行),要点包括:
type字段取值Py_T_SHORT、Py_T_INT、Py_T_LONG、Py_T_DOUBLE、Py_T_STRING、Py_T_CHAR、Py_T_OBJECT_EX、Py_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_NewMethod为type从PyMethodDef创建方法描述符,把 C 函数暴露为类型上的方法。这是tp_methods条目创建的那类描述符,在 Python 中呈现为types.MethodDescriptorType。PyDescr_NewClassMethod创建类方法描述符,对应tp_methods中带有METH_CLASS标志的条目。类方法描述符在访问时绑定的是类而不是实例,呈现为types.ClassMethodDescriptorType。
PyDescr_NewMethod的实现(Objects/descrobject.c 第 934–978 行)有一个值得注意的细节:它根据ml_flags中的调用约定(METH_VARARGS、METH_FASTCALL、METH_NOARGS、METH_O、METH_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);为type从wrapperbase结构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_base与d_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 追踪对象并强引用保存descr与self两个字段。
描述符的内部结构: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.rst 对PyDescr_COMMON宏给出了重要告诫:
This was included in Python's C API by mistake; do not use it in extensions.
该宏被错误地纳入了 Python C API,文档标注其于 3.15 起软弃用(soft-deprecated)。如果你在编写自定义描述符类型,文档建议的正确做法是:定义一个自己的类,实现描述符协议,即设置PyTypeObject的tp_descr_get与tp_descr_set两个槽,而不是套用PyDescr_COMMON布局。
描述符如何进入类型字典:CPython 的内部流程
CPython 在类型初始化时遍历tp_methods、tp_members、tp_getset数组,为每条记录创建描述符并写入类型字典。这一过程在 Objects/typeobject.c 中:
type_add_members(第 8576–8597 行):遍历type->tp_members,对每条记录调用PyDescr_NewMember(type, memb),再用PyDict_SetDefaultRef以PyDescr_NAME(descr)为名存入类型字典——SetDefault语义意味着同名的 Python 层覆盖不会被 C 层记录冲掉;type_add_getset(第 8600–8622 行):同样的模式调用PyDescr_NewGetSet(type, gsp);type_add_method(约第 8480–8549 行):按ml_flags分派——METH_CLASS走PyDescr_NewClassMethod,METH_STATIC走PyStaticMethod_New(注意它不是PyDescrObject派生类型),其余走PyDescr_NewMethod。
所有创建路径最终汇聚到统一的工厂函数descr_new(Objects/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_get(Objects/descrobject.c 第 162–181 行)展示了完整协议流程:
obj == NULL(通过类访问):返回描述符自身的新引用(Py_NewRef(descr))——这就是Type.attr返回描述符本身的原因;descr_check:校验实例是否属于d_type,不匹配则抛出形如descriptor 'x' for 'Y' objects doesn't apply to a 'Z' object的TypeError;- 若
PyMemberDef.flags带Py_AUDIT_READ,先触发PySys_Audit("object.__getattr__", ...)审计; - 调用
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 行)。property的fget/fset/fdel属性正是用PyMemberDef以Py_READONLY标志暴露的(第 1582–1588 行)——一个描述符类型内部又使用成员描述符的典型嵌套。
另外,Objects/descrobject.c 中还有PyDictProxy_Type(第 24 行声明的mappingproxy,即Type.__dict__的只读代理类型),文件内第 1042 行起的注释也承认它“没有理由放在这个文件里,只是新增文件有点麻烦”——阅读该文件时可以把它视为随附的只读映射代理实现。
实践:在 C 扩展中定义描述符属性的完整示例
下面是一个最小 C 扩展片段,展示如何用tp_members与tp_getset数组声明描述符(无需手动调用PyDescr_New*函数,类型初始化会自动完成,见上文 Objects/typeobject.c 的type_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.count是types.MemberDescriptorType(只读,实例字典无法遮蔽),counter.total是types.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并置错; - 两个工具 API:
PyDescr_IsData通过tp_descr_set != NULL判定数据描述符(无错误检查);PyWrapper_New生成 slot wrapper 的绑定形式(types.MethodWrapperType); - 内置描述符:
PyProperty_Type、PySuper_Type、PyClassMethod_Type、PyStaticMethod_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_get、getset_get、method_get等实现; - 注意事项:
PyDescr_COMMON宏属于历史误入 C API 的部分,3.15 起软弃用,自定义描述符请直接实现tp_descr_get/tp_descr_set协议;PyDescr_NewMember拒绝Py_RELATIVE_OFFSET;PyDescr_NewMethod会在创建期校验调用约定 flags 并缓存 vectorcall 路径。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考