如何在Python C-API中动态创建派生类型
假设我们有一个叫做 Noddy
的类型,具体定义可以参考 这个关于为Python编写C扩展模块的教程。现在我们想要创建一个派生类型,只覆盖 Noddy
的 __new__()
方法。
目前我使用的方式如下(为了易读性,省略了错误检查):
PyTypeObject *BrownNoddyType =
(PyTypeObject *)PyType_Type.tp_alloc(&PyType_Type, 0);
BrownNoddyType->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
BrownNoddyType->tp_name = "noddy.BrownNoddy";
BrownNoddyType->tp_doc = "BrownNoddy objects";
BrownNoddyType->tp_base = &NoddyType;
BrownNoddyType->tp_new = BrownNoddy_new;
PyType_Ready(BrownNoddyType);
这个方法可以工作,但我不确定这是不是正确的做法。我本以为我也需要设置 Py_TPFLAGS_HEAPTYPE
标志,因为我是在堆上动态分配类型对象,但这样做会导致解释器崩溃。
我还考虑过使用 PyObject_Call()
或类似的方法显式调用 type()
,但我放弃了这个想法。我需要把 BrownNoddy_new()
函数包装成一个Python函数对象,并创建一个字典将 __new__
映射到这个函数对象,这样做似乎有点傻。
那么,最好的做法是什么呢?我的方法正确吗?有没有我遗漏的接口函数?
更新
在python-dev邮件列表上有两个相关主题的讨论 (1) (2)。从这些讨论和一些实验中,我推测除非类型是通过调用 type()
分配的,否则我不应该设置 Py_TPFLAGS_HEAPTYPE
。这些讨论中对手动分配类型和调用 type()
的推荐意见不一。如果我知道如何将应该放在 tp_new
插槽中的C函数包装起来,我会更倾向于后者。对于普通方法,这一步很简单——我可以直接使用 PyDescr_NewMethod()
来获取合适的包装对象。不过,我不知道如何为我的 __new__()
方法创建这样的包装对象——也许我需要那个未记录的函数 PyCFunction_New()
来创建这样的包装对象。
3 个回答
想要理解怎么做这个,可以试着用SWIG创建一个版本。看看它生成了什么,是否和你想要的结果一致,或者是用其他方式实现的。从我所了解的,写SWIG的人对如何扩展Python有很深的理解。无论如何,看看他们是怎么做的总是有帮助的,这可能会帮助你理解这个问题。
如果我的回答不好,我先道个歉。不过,你可以在 PythonQt 找到这个想法的实现,特别是以下这些文件可能对你有帮助:
我觉得从 PythonQtClassWrapper_init 中的这一段代码特别有意思:
static int PythonQtClassWrapper_init(PythonQtClassWrapper* self, PyObject* args, PyObject* kwds)
{
// call the default type init
if (PyType_Type.tp_init((PyObject *)self, args, kwds) < 0) {
return -1;
}
// if we have no CPP class information, try our base class
if (!self->classInfo()) {
PyTypeObject* superType = ((PyTypeObject *)self)->tp_base;
if (!superType || (superType->ob_type != &PythonQtClassWrapper_Type)) {
PyErr_Format(PyExc_TypeError, "type %s is not derived from PythonQtClassWrapper", ((PyTypeObject*)self)->tp_name);
return -1;
}
// take the class info from the superType
self->_classInfo = ((PythonQtClassWrapper*)superType)->classInfo();
}
return 0;
}
值得注意的是,PythonQt 确实使用了一个包装生成器,所以这并不完全符合你所问的。不过我个人认为,试图绕过虚表并不是最优的设计。简单来说,有很多不同的 C++ 包装生成器可以用来和 Python 一起使用,人们使用它们是有原因的——它们有文档,有很多示例可以在搜索结果和 Stack Overflow 上找到。如果你自己手动做了一个别人没见过的解决方案,当他们遇到问题时,调试起来会更加困难。即使是闭源的,下一个需要维护它的人也会感到困惑,而你还得向每一个新来的人解释。
一旦你让代码生成器工作起来,你只需要维护底层的 C++ 代码,不需要手动更新或修改你的扩展代码。(这可能和你选择的诱人解决方案相差不远)
所提的解决方案是一个例子,说明了打破新引入的 PyCapsule 提供的类型安全性(在按指示使用时,提供了更多保护)。
所以,虽然这样实现派生类/子类是可能的,但从长远来看,这可能不是最好的选择。更好的做法是包装代码,让虚表发挥它的作用,当新来的同事有问题时,你可以直接指给他看 相关文档,无论是 哪个解决方案,还是 最合适的 方案。
不过这只是我的个人看法。 :D
我在修改一个扩展程序以兼容Python 3时遇到了同样的问题,后来在寻找解决办法时找到了这个页面。
最终,我通过阅读Python解释器的源代码、PEP 0384和C-API的文档解决了这个问题。
设置Py_TPFLAGS_HEAPTYPE
这个标志是告诉解释器把你的PyTypeObject
重新转换为PyHeapTypeObject
,后者包含一些额外的成员,这些成员也必须分配内存。如果你不分配这些额外的内存,解释器在某个时刻尝试访问这些成员时就会出错,导致程序崩溃。
Python 3.2引入了C结构PyType_Slot
和PyType_Spec
,以及C函数PyType_FromSpec
,这些都简化了动态类型的创建。简单来说,你可以用PyType_Slot
和PyType_Spec
来指定PyTypeObject
的tp_*
成员,然后调用PyType_FromSpec
来处理分配和初始化内存的工作。
根据PEP 0384,我们有:
typedef struct{
int slot; /* slot id, see below */
void *pfunc; /* function pointer */
} PyType_Slot;
typedef struct{
const char* name;
int basicsize;
int itemsize;
int flags;
PyType_Slot *slots; /* terminated by slot==0. */
} PyType_Spec;
PyObject* PyType_FromSpec(PyType_Spec*);
(上面的内容并不是PEP 0384的逐字复制,PEP 0384中还包括const char *doc
作为PyType_Spec
的一个成员,但在源代码中并没有这个成员。)
在原始示例中使用这些内容,假设我们有一个C结构BrownNoddy
,它扩展了基类Noddy
的C结构。那么我们会有:
PyType_Slot slots[] = {
{ Py_tp_doc, "BrownNoddy objects" },
{ Py_tp_base, &NoddyType },
{ Py_tp_new, BrownNoddy_new },
{ 0 },
};
PyType_Spec spec = { "noddy.BrownNoddy", sizeof(BrownNoddy), 0,
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, slots };
PyTypeObject *BrownNoddyType = (PyTypeObject *)PyType_FromSpec(&spec);
这应该能完成原始代码中的所有工作,包括调用PyType_Ready
,以及创建动态类型所需的内容,包括设置Py_TPFLAGS_HEAPTYPE
,并为PyHeapTypeObject
分配和初始化额外的内存。
希望这些信息对你有帮助。