news 2026/9/7 9:21:30

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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_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_Typetypes.GetSetDescriptorTypePyGetSetDef创建的 getter/setter 描述符
PyMemberDescr_Typetypes.MemberDescriptorTypePyMemberDef创建的 C 结构体字段描述符
PyMethodDescr_Typetypes.MethodDescriptorTypePyMethodDef创建的方法描述符
PyWrapperDescr_Typetypes.WrapperDescriptorType暴露类型槽(slot)实现的特殊方法,如__repr____add__
PyClassMethodDescr_Typetypes.ClassMethodDescriptorTypeMETH_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.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)。如果你在编写自定义描述符类型,文档建议的正确做法是:定义一个自己的类,实现描述符协议,即设置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_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 行)展示了完整协议流程:

  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_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 行)。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.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.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 路径。

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 9:20:49

Playwright+MCP+Agent Browser:AI驱动的Web自动化实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:16:28

双向反射分布函数(BRDF):材质反射特性的数学建模与工程实现

一、开场:三种材质,三种「性格」 想象你把一把手电筒照向三种不同的表面—— 一面镀铬的镜子:光几乎原样反射回一个特定方向,你稍微偏移视角,反射光斑瞬间消失; 一张白纸:光被均匀地散射到各个方向,无论你从哪个角度看,亮度都差不多; 一块毛玻璃:光既有集中反射的高…

作者头像 李华
网站建设 2026/9/7 9:15:12

CS2比赛数据可视化:用Python和Pandas解析HLTV数据

抱歉&#xff0c;这个标题不适合改写成 CSDN 技术博客。原因很简单&#xff1a;标题内容属于电竞选手个人相关话题&#xff0c;并且包含对真实人物进行“NPD”心理特征标签化判断的表述。这类内容不符合 CSDN 技术平台的内容规范&#xff0c;也不满足版权、肖像、名誉方面的合规…

作者头像 李华
网站建设 2026/9/7 9:13:06

PSD导入引擎实战:图层原位还原与按钮交互绑定全解析

PSD 导入引擎这个方向&#xff0c;过去最大的问题是“导完就废了”。设计稿里的图层层级、混合模式、按钮状态、交互跳转&#xff0c;到了前端或者游戏引擎里全部归零&#xff0c;只能重新照着稿子手搭一遍。现在有项目把PSD 解析、图层原位保留、按钮交互绑定放在一起做成引擎…

作者头像 李华