FEATURED · 精选文章

CPython 扩展类型开发指南:PyTypeObject 各类型方法的实现原理与实战详解

发布时间 / 2026/9/7 1:56:13
来源 / 创域科博编辑部
栏目 / 资讯中心
CPython 扩展类型开发指南:PyTypeObject 各类型方法的实现原理与实战详解 CPython 扩展类型开发指南PyTypeObject 各类型方法的实现原理与实战详解【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文以 CPython 官方扩展文档 Defining Extension Types: Assorted TopicsDoc/extending/newtypes.rst为主体系统讲解PyTypeObject中各tp_类型方法的职责、签名与实现要点并结合 CPython 源码仓库中的真实类型定义如 xxsubtype.c、listobject.c和头文件object.h逐一印证。读完本文你将能够为自己的 C 扩展定义完整的堆类型/内置类型实现内存释放、对象展示、属性管理、比较、抽象协议数字/序列/映射/迭代器以及弱引用支持。PyTypeObject 全貌字段总览与阅读策略CPython 用一个核心结构体PyTypeObject描述类型本身。官方文档在介绍各类型方法前首先给出完整定义节选自 Doc/includes/typestruct.h部分仅调试构建使用的字段已省略typedef struct _typeobject { PyObject_VAR_HEAD const char *tp_name; /* For printing, in format module.name */ Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ /* Methods to implement standard operations */ destructor tp_dealloc; Py_ssize_t tp_vectorcall_offset; getattrfunc tp_getattr; setattrfunc tp_setattr; PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2) or tp_reserved (Python 3) */ reprfunc tp_repr; /* Method suites for standard classes */ PyNumberMethods *tp_as_number; PySequenceMethods *tp_as_sequence; PyMappingMethods *tp_as_mapping; /* More standard operations (here for binary compatibility) */ hashfunc tp_hash; ternaryfunc tp_call; reprfunc tp_str; getattrofunc tp_getattro; setattrofunc tp_setattro; /* Functions to access object as input/output buffer */ PyBufferProcs *tp_as_buffer; /* Flags to define presence of optional/expanded features */ unsigned long tp_flags; const char *tp_doc; /* Documentation string */ /* Assigned meaning in release 2.0 */ /* call function for all accessible objects */ traverseproc tp_traverse; /* delete references to contained objects */ inquiry tp_clear; /* Assigned meaning in release 2.1 */ /* rich comparisons */ richcmpfunc tp_richcompare; /* weak reference enabler */ Py_ssize_t tp_weaklistoffset; /* Iterators */ getiterfunc tp_iter; iternextfunc tp_iternext; /* Attribute descriptor and subclassing stuff */ PyMethodDef *tp_methods; PyMemberDef *tp_members; PyGetSetDef *tp_getset; // Strong reference on a heap type, borrowed reference on a static type PyTypeObject *tp_base; PyObject *tp_dict; descrgetfunc tp_descr_get; descrsetfunc tp_descr_set; Py_ssize_t tp_dictoffset; initproc tp_init; allocfunc tp_alloc; newfunc tp_new; freefunc tp_free; /* Low-level free-memory routine */ inquiry tp_is_gc; /* For PyObject_IS_GC */ PyObject *tp_bases; PyObject *tp_mro; /* method resolution order */ PyObject *tp_cache; /* no longer used */ void *tp_subclasses; /* for static builtin types this is an index */ PyObject *tp_weaklist; /* not used for static builtin types */ destructor tp_del; /* Type attribute cache version tag. Added in version 2.6. * If zero, the cache is invalid and must be initialized. */ unsigned int tp_version_tag; destructor tp_finalize; vectorcallfunc tp_vectorcall; /* bitset of which type-watchers care about this type */ unsigned char tp_watched; /* Number of tp_version_tag values used. * Set to _Py_ATTR_CACHE_UNUSED if the attribute cache is * disabled for this type (e.g. due to custom MRO entries). * Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere). */ uint16_t tp_versions_used; } PyTypeObject;方法数量非常多但官方文档给出的建议是你几乎只需要实现其中的一小部分。而且字段顺序受大量历史包袱影响例如tp_repr排在中段、tp_hash/tp_call/tp_str被单独归为binary compatibility区块因此不必按结构体定义顺序逐个理解。官方推荐的最快上手方式是找一个包含你所需字段的现成类型示例再改写字段值。在 CPython 源码仓库中最好的范本就在 Objects 目录各种内建类型的实现和 Modules 目录如 Modules/xxsubtype.c 这类演示类型。基础字段tp_name、tp_basicsize、tp_itemsize、tp_docconst char *tp_name; /* For printing */类型名称。正如扩展开发入门文档Doc/extending/first-extension-module.rst所述这个名字几乎全部用于诊断输出traceback、repr 缺省值等因此请选一个在出错排查时真正有帮助的名字。Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */这两个字段告诉运行时为该类型创建新实例时分配多少内存。Python 内建支持变长结构典型如字符串、元组tp_itemsize字段就是为此设计的用于变长部分的分配计算。const char *tp_doc;在这里填入一个字符串或其地址当 Python 代码通过obj.__doc__读取文档字符串时即返回它。终结化与释放tp_dealloc 的正确写法destructor tp_dealloc;该函数在实例引用计数降为零、解释器需要回收该对象时被调用。如果你的类型有需要释放的内存或其他清理工作就写在这里——对象本身也必须在这里释放。最简示例static void newdatatype_dealloc(PyObject *op) { newdatatypeobject *self (newdatatypeobject *) op; free(self-obj_UnderlyingDatatypePtr); Py_TYPE(self)-tp_free(self); }若类型支持垃圾回收GC析构函数在清空任何成员字段之前必须先调用PyObject_GC_UnTrackstatic void newdatatype_dealloc(PyObject *op) { newdatatypeobject *self (newdatatypeobject *) op; PyObject_GC_UnTrack(op); Py_CLEAR(self-other_obj); ... Py_TYPE(self)-tp_free(self); }这一要求在源码中有直接印证Objects/listobject.c 中list_dealloc的第一行就是PyObject_GC_UnTrack(op)随后才逐个Py_CLEAR列表元素并释放对象。必须保护未决异常PyErr_Fetch / PyErr_Restoretp_dealloc有一个重要约束不得动leave alone任何当前已存在的未决异常。原因是析构函数经常在解释器回卷 Python 栈的过程中被调用当栈因异常而非正常返回被回卷时没有任何机制保护析构函数不看到一个已经置位的异常。析构中执行的任何可能触发额外 Python 代码的操作比如调用回调都可能检测到异常已置位的状态从而产生误导性的解释器错误。正确的防护方式是在执行业务操作前用PyErr_Fetch保存未决异常完成后用PyErr_Restore恢复static void my_dealloc(PyObject *obj) { MyObject *self (MyObject *) obj; PyObject *cbresult; if (self-my_callback ! NULL) { PyObject *err_type, *err_value, *err_traceback; /* This saves the current exception state */ PyErr_Fetch(err_type, err_value, err_traceback); cbresult PyObject_CallNoArgs(self-my_callback); if (cbresult NULL) { PyErr_WriteUnraisable(self-my_callback); } else { Py_DECREF(cbresult); } /* This restores the saved exception state */ PyErr_Restore(err_type, err_value, err_traceback); Py_DECREF(self-my_callback); } Py_TYPE(self)-tp_free(self); }官方文档同时给出了两条必须记住的限制原文以 note 形式强调GC 类型注意如果你的类型支持垃圾回收实现了tp_traverse和/或tp_clear等到tp_dealloc被调用时对象的某些成员可能已经被清空或终结化了不稳定的引用计数状态在tp_dealloc中对象引用计数为零处于不稳定状态。对非平凡对象或 API 的任何调用如上面的回调示例都可能再次触发tp_dealloc导致双重释放和崩溃。正因为这些限制自 Python 3.4 起官方建议不要在tp_dealloc里放复杂终结化代码而应改用新的tp_finalize类型方法参见 PEP 442。在上面的PyTypeObject定义中可以看到destructor tp_finalize;字段就位于结构体尾部。对象展示tp_repr 与 tp_strPython 生成对象文本表示有两种方式repr()与str()print()只是调用str()。这两个处理器都是可选的reprfunc tp_repr; reprfunc tp_str;tp_repr应返回一个包含该实例表示的字符串对象。简单示例static PyObject * newdatatype_repr(PyObject *op) { newdatatypeobject *self (newdatatypeobject *) op; return PyUnicode_FromFormat(Repr-ified_newdatatype{{size:%d}}, self-obj_UnderlyingDatatypePtr-size); }注意格式串中{{与}}是PyUnicode_FromFormat的花括号转义写法。如果未提供tp_repr解释器会使用类型名tp_name加上一个可唯一标识对象的值即地址来生成缺省表示——这也是为什么tp_name值得认真取。tp_str之于str就像tp_repr之于repr当 Python 代码对实例调用str()时被调用实现与tp_repr非常类似但结果字符串是面向人类阅读的。如果未指定tp_str则回退使用tp_repr。static PyObject * newdatatype_str(PyObject *op) { newdatatypeobject *self (newdatatypeobject *) op; return PyUnicode_FromFormat(Stringified_newdatatype{{size:%d}}, self-obj_UnderlyingDatatypePtr-size); }属性管理两套接口与泛型机制对于每一个支持属性的对象其类型必须提供控制属性解析方式的函数一个用于读取属性如果定义了属性另一个用于写入属性如果允许设置。删除属性是一种特殊情形此时传给处理器的新值参数为NULL。Python 提供两对属性处理器类型只需实现其中一对。区别在于一对以char *形式接收属性名另一对以PyObject *形式接收。类型可按实现便利性任选其一getattrfunc tp_getattr; /* char * version */ setattrfunc tp_setattr; /* ... */ getattrofunc tp_getattro; /* PyObject * version */ setattrofunc tp_setattro;如果对象属性访问始终是简单操作见下可以直接使用 CPython 内置的泛型实现来提供PyObject *版本的属性管理。实际上自 Python 2.2 引入泛型机制后真正需要类型专属属性处理器的场景已几乎绝迹——尽管仓库中仍保留着许多未迁移到泛型机制的旧式示例。泛型属性管理Generic Attribute Management大多数扩展类型只使用简单属性。简单的条件只有两条属性名在调用PyType_Ready时就已经已知记录属性被查找/被设置这一事实不需要特殊处理也不需要基于值执行任何动作。注意该列表不限制属性值的类型、值的计算时机以及相关数据的存储方式。当PyType_Ready被调用时它使用类型对象引用的三张表来创建描述符descriptor并把描述符放入类型对象的字典中。每个描述符控制对实例对象一个属性的访问。三张表都是可选的如果全部为NULL该类型的实例就只有从基类继承来的属性此时tp_getattro与tp_setattro也应保持NULL让基类处理属性。三张表声明为类型对象的三个字段struct PyMethodDef *tp_methods; struct PyMemberDef *tp_members; struct PyGetSetDef *tp_getset;方法表 tp_methods若非NULL必须指向一个PyMethodDef结构体数组。每个条目为该结构的一个实例typedef struct PyMethodDef { const char *ml_name; /* method name */ PyCFunction ml_meth; /* implementation function */ int ml_flags; /* flags */ const char *ml_doc; /* docstring */ } PyMethodDef;类型提供的每个方法对应一个条目从基类继承的方法无需条目。数组末尾必须追加一个**哨兵sentinel**条目其ml_name字段为NULL。成员表 tp_members用于定义直接映射到实例数据中的属性支持多种基础 C 类型可设为只读或可读写。表中的结构定义为typedef struct PyMemberDef { const char *name; int type; int offset; int flags; const char *doc; } PyMemberDef;表中每个条目都会构造一个描述符加入类型用于从实例结构体中抽取值。type字段应填入Py_T_INT、Py_T_DOUBLE之类的类型码它决定 Python 值与 C 值之间如何转换flags字段控制属性的访问方式——设为Py_READONLY可禁止 Python 代码对该属性赋值。使用tp_members表构建描述符的一个实用好处是这样定义的每个属性都可以携带文档字符串只需在表中提供文本。应用程序可以通过内省 API 从类对象上取出描述符并用其__doc__属性读取该文档。与tp_methods表一样末尾需要一个name值为NULL的哨兵条目。在仓库中Modules/xxsubtype.c 展示了spamdict类型的真实用法tp_members指向spamdict_members表定义了一批只读Py_T_OBJECT成员tp_getset为NULL其余槽位如tp_repr、tp_richcompare、tp_iter均显式置 0——这正是官方找一个现成示例再改建议的典型体现。关于描述符本身描述符对象有两个与tp_getattro/tp_setattro对应的处理器函数。__get__()接收描述符、实例与类型对象返回属性值失败则返回NULL并置异常__set__()接收描述符、实例、类型与新值。类型专属属性管理Type-specific Attribute Management为简单起见此处演示char *版本它与PyObject *版本的唯一区别就是 name 参数的类型。下面的示例在效果上等价于泛型示例但没有使用 Python 2.2 引入的泛型支持——它解释了这些处理器函数如何被调用这样当你确实需要扩展其功能时能明白该做什么。tp_getattr在对象需要属性查找时被调用调用场景与 Python 类中__getattr__方法被调用的场景相同static PyObject * newdatatype_getattr(PyObject *op, char *name) { newdatatypeobject *self (newdatatypeobject *) op; if (strcmp(name, data) 0) { return PyLong_FromLong(self-data); } PyErr_Format(PyExc_AttributeError, %.100s object has no attribute %.400s, Py_TYPE(self)-tp_name, name); return NULL; }tp_setattr在类的实例上调用__setattr__或__delattr__时执行。当要删除属性时第三个参数为NULL。下面是一个直接抛异常的实现——如果你的全部需求就是拒绝设置其实把tp_setattr设为NULL即可static int newdatatype_setattr(PyObject *op, char *name, PyObject *v) { PyErr_Format(PyExc_RuntimeError, Read-only attribute: %s, name); return -1; }对象比较tp_richcomparerichcmpfunc tp_richcompare;该处理器在需要比较时被调用与富比较方法rich comparison methods如__lt__等对应同时也被 C API 函数PyObject_RichCompare和PyObject_RichCompareBool调用。函数接收两个 Python 对象和运算符作为参数运算符取值为Py_EQ、Py_NE、Py_LE、Py_GE、Py_LT或Py_GT。它应按指定运算符比较两个对象并在比较成功时返回Py_True或Py_False当比较未实现、应改试另一对象的比较方法时返回Py_NotImplemented置位异常时返回NULL。下面是一个示例实现对一个内部指针的 size 相等即视为相等的数据类型static PyObject * newdatatype_richcmp(PyObject *lhs, PyObject *rhs, int op) { newdatatypeobject *obj1 (newdatatypeobject *) lhs; newdatatypeobject *obj2 (newdatatypeobject *) rhs; PyObject *result; int c, size1, size2; /* code to make sure that both arguments are of type newdatatype omitted */ size1 obj1-obj_UnderlyingDatatypePtr-size; size2 obj2-obj_UnderlyingDatatypePtr-size; switch (op) { case Py_LT: c size1 size2; break; case Py_LE: c size1 size2; break; case Py_EQ: c size1 size2; break; case Py_NE: c size1 ! size2; break; case Py_GT: c size1 size2; break; case Py_GE: c size1 size2; break; } result c ? Py_True : Py_False; return Py_NewRef(result); }注意最后用Py_NewRef(result)增加引用计数后返回——Py_True/Py_False是共享单例直接把引用计数 1 是返回它们的正确方式。抽象协议支持数字、序列、映射、哈希、调用与迭代器Python 支持一系列抽象协议abstract protocols具体接口在官方 C API 文档的 abstract 章节Doc/c-api/abstract.rst中有说明。其中一部分抽象接口number、mapping、sequence自 Python 诞生之初就存在其他协议则随时间陆续加入。实现机制上有个历史分界对早期协议类型对象以可选的处理器块optional blocks of handlers形式引用它们对较新的协议主类型对象中增加了额外槽位并在tp_flags中置位一个标志位来表示槽位存在、解释器应检查它们。注意标志位并不表示槽位值非NULL——标志置位仅表示槽位存在槽位本身仍可能未填充。PyNumberMethods *tp_as_number; PySequenceMethods *tp_as_sequence; PyMappingMethods *tp_as_mapping;若希望对象表现得像数字、序列或映射就分别放入实现了PyNumberMethods、PySequenceMethods或PyMappingMethods的结构体地址由你自己决定如何填充。CPython 源码发行版的Objects目录中可以找到每一种协议的完整使用示例如 Objects/listobject.c 同时填入了tp_as_sequence、tp_as_mapping与tp_as_buffer。tp_hash哈希hashfunc tp_hash;如果选择提供该函数它应为你的数据类型的一个实例返回哈希值。简单示例static Py_hash_t newdatatype_hash(PyObject *op) { newdatatypeobject *self (newdatatypeobject *) op; Py_hash_t result; result self-some_size 32767 * self-some_number; if (result -1) { result -2; } return result; }Py_hash_t是一个宽度随平台变化的有符号整数类型。从tp_hash返回-1表示出错所以计算成功时要刻意避开-1如上例改为返回-2。tp_call实例可调用ternaryfunc tp_call;当你的数据类型实例被调用时触发该函数。例如obj1是该类型实例、脚本中有obj1(hello)则tp_call处理器被调用。函数接收三个参数self被调用的数据类型实例本身即obj1args包含调用参数组的元组可用PyArg_ParseTuple提取kwds传入的关键字参数字典。若非NULL且你支持关键字参数用PyArg_ParseTupleAndKeywords提取若不想支持关键字参数而它非NULL应抛出TypeError并说明不支持关键字参数。一个玩具级tp_call实现static PyObject * newdatatype_call(PyObject *op, PyObject *args, PyObject *kwds) { newdatatypeobject *self (newdatatypeobject *) op; PyObject *result; const char *arg1; const char *arg2; const char *arg3; if (!PyArg_ParseTuple(args, sss:call, arg1, arg2, arg3)) { return NULL; } result PyUnicode_FromFormat( Returning -- value: [%d] arg1: [%s] arg2: [%s] arg3: [%s]\n, self-obj_UnderlyingDatatypePtr-size, arg1, arg2, arg3); return result; }tp_iter 与 tp_iternext迭代器协议/* Iterators */ getiterfunc tp_iter; iternextfunc tp_iternext;这两个函数共同支持迭代器协议。它们都恰好接收一个参数正在为其调用的实例并返回新引用出错时置位异常并返回NULL。tp_iter对应 Python 的__iter__方法tp_iternext对应__next__方法。规则如下任何可迭代对象必须实现tp_iter且必须返回一个迭代器对象。与 Python 类的约定一致可支持多个独立迭代器的集合如 list、tuple每次调用tp_iter都应新建并返回一个迭代器只能被迭代一次的、有副作用的对象典型如文件对象tp_iter可直接返回自身的新引用因此还必须实现tp_iternext。任何迭代器对象都应同时实现tp_iter与tp_iternext。迭代器的tp_iter应返回迭代器自身的新引用其tp_iternext应返回下一个元素的新引用若存在。迭代到达末尾时tp_iternext可以不置异常直接返回NULL也可以在返回NULL的同时置StopIteration省略异常能获得略好的性能若发生真正的错误则必须置位异常并返回NULL。弱引用支持Py_TPFLAGS_MANAGED_WEAKREFPython 弱引用实现的一个目标是允许任何类型参与弱引用机制而不会给性能敏感对象如数字带来开销。相关 Python 侧接口参见weakref模块文档Doc/library/weakref.rst。一个对象要可被弱引用扩展类型必须在tp_flags字段中置位Py_TPFLAGS_MANAGED_WEAKREF并把遗留的tp_weaklistoffset字段保持为零。如果设置了该标志Py_TPFLAGS_HAVE_GC也应同时设置。在 Include/object.h 中可以看到该标志的当前定义与注释/* Placement of weakref pointers are managed by the VM, not by the type. * The VM will automatically set tp_weaklistoffset. Implies Py_TPFLAGS_HAVE_GC. */ #define Py_TPFLAGS_MANAGED_WEAKREF (1 3)注释明确指出弱引用指针的存放位置由虚拟机管理解释器会自动设置tp_weaklistoffset。静态声明的类型对象形态如下static PyTypeObject TrivialType { PyVarObject_HEAD_INIT(NULL, 0) /* ... other members omitted for brevity ... */ .tp_flags Py_TPFLAGS_MANAGED_WEAKREF | ..., };除此之外唯一的补充是tp_dealloc需要清除弱引用调用PyObject_ClearWeakRefsstatic void Trivial_dealloc(PyObject *op) { /* Clear weakrefs first before calling any destructors */ PyObject_ClearWeakRefs(op); /* ... remainder of destruction code omitted for brevity ... */ Py_TYPE(op)-tp_free(op); }更多实践建议官方文档给出的两条学习路径建议值得直接遵循1. 去 CPython 源码里搜tp_前缀找范本。想学会实现某个具体方法就打开Objects目录在 C 源码中搜索tp_加上目标方法名例如tp_richcompare即可找到你想要的函数实现示例。从源码结构看各内建类型list、dict、tuple、unicode 等都完整填充了这些槽位是最好的活教材。2. 验证对象是否为具体类型实例用PyObject_TypeCheckif (!PyObject_TypeCheck(some_object, MyType)) { PyErr_SetString(PyExc_TypeError, arg #1 not a mything); return NULL; }延伸阅读扩展模块开发总览Doc/extending/extending.rst第一个扩展模块的完整步骤Doc/extending/first-extension-module.rst手把手新类型教程本节的姊妹篇Doc/extending/newtypes_tutorial.rstPyTypeObject完整定义Doc/includes/typestruct.h类型标志位定义Include/object.h真实类型定义示例Modules/xxsubtype.c泛型属性表 显式置空槽位的写法、Objects/listobject.cGC 类型 dealloc、序列/映射抽象协议小结PyTypeObject字段虽多扩展类型开发实际用到的核心组合是tp_dealloc含 GC 取消跟踪、弱引用清理与异常状态保护、tp_repr/tp_str、三张属性表tp_methods/tp_members/tp_getset或tp_getattro/tp_setattro、tp_richcompare、按需填充的抽象协议结构体tp_as_number/tp_as_sequence/tp_as_mapping、tp_hash、tp_call、tp_iter/tp_iternext以及Py_TPFLAGS_MANAGED_WEAKREF标志。自 Python 3.4 起复杂终结化逻辑应放在tp_finalize而非tp_dealloc。掌握这些字段与槽位的职责后再配合Objects目录中的现成范本即可为任意 C 数据结构构建行为完整的 Python 类型。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻