ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

CPython 描述符对象 C API 详解:从 PyDescr_New 系列函数到描述符协议实现 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_Typetypes.GetSetDescriptorType由PyGetSetDef创建的 getter/setter 描述符PyMemberDescr_Typetypes.MemberDescriptorType由PyMemberDef创建的 C 结构体字段描述符PyMethodDescr_Typetypes.MethodDescriptorType由PyMethodDef创建的方法描述符PyWrapperDescr_Typetypes.WrapperDescriptorType暴露类型槽slot实现的特殊方法如__repr__、__add__PyClassMethodDescr_Typetypes.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_NewGetSetC 级 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_NewMemberC 结构体字段描述符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标志并抛出SystemErrorif (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 行属于内部头文件不暴露给受限 APIstruct 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 Pythons 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_NewClassMethodMETH_STATIC走PyStaticMethod_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 行展示了完整协议流程obj NULL通过类访问返回描述符自身的新引用Py_NewRef(descr)——这就是Type.attr返回描述符本身的原因descr_check校验实例是否属于d_type不匹配则抛出形如descriptor x for Y objects doesnt 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、staticmethodDoc/c-api/descriptor.rst 的 “Built-in descriptors” 一节列出了 C 层可直接使用的内置描述符类型对象C API 符号Python 层对应说明PyProperty_Typepropertyproperty 对象的类型对象两个符号是同一个对象PySuper_Typesupersuper 对象的类型对象同为同一对象PyClassMethod_Typeclassmethodclassmethod 对象的类型PyClassMethodDescr_Typetypes.ClassMethodDescriptorTypeC 层类方法描述符类型对应 C 扩展类型中定义classmethod时创建的描述符PyStaticMethod_Typestaticmethodstaticmethod 对象的类型配套的两个构造函数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_getsettypedef 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并置错两个工具 APIPyDescr_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负责类型强引用与名称 interntype_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_OFFSETPyDescr_NewMethod会在创建期校验调用约定 flags 并缓存 vectorcall 路径。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进