如何在 Python C-API 中动态创建派生类型

假设我们有 为 Python 编写 C 扩展模块的教程中定义的类型 Noddy。现在我们要创建一个派生类型,只覆盖 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标志,因为我在堆上动态分配类型对象,但这样做会导致解释器中出现 Segfault。

我还考虑过使用 PyObject_Call()或类似方法显式调用 type(),但我放弃了这个想法。我需要将函数 BrownNoddy_new()包装在一个 Python 函数对象中,并创建一个将 __new__映射到这个函数对象的字典,这看起来很愚蠢。

最好的办法是什么?我的方法正确吗?我是不是漏了什么接口功能?

更新

在 python-dev 邮件列表 (1)(2)中有两个关于相关主题的线程。从这些线程和一些实验中,我推断,除非通过调用 type()来分配类型,否则我不应该设置 Py_TPFLAGS_HEAPTYPE。无论是手动分配类型还是调用 type(),这些线程中都有不同的建议。如果我知道包装应该放在 tp_new插槽中的 C 函数的推荐方法是什么,我会对后者感到满意。对于常规方法,这一步很简单——我可以只使用 PyDescr_NewMethod()来获得一个合适的包装器对象。但是,我不知道如何为我的 __new__()方法创建这样一个包装器对象——也许我需要没有文档说明的函数 PyCFunction_New()来创建这样一个包装器对象。

5596 次浏览

尝试和理解如何做到这一点的一种方法是使用 SWIG 创建它的一个版本。看看它产生了什么,看看它是否匹配或以不同的方式完成。据我所知,撰写 SWIG 的人对扩展 Python 有着深刻的理解。无论如何,看看他们是怎么做事的也无妨。它可以帮助你理解这个问题。

如果这个答案很糟糕,我先道歉,但是你可以在 Python中找到这个想法的实现,特别是我认为下面的文件可能是有用的参考:

Python QtClassWrapper _ 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;
}

值得注意的是,Python Qt 确实使用了包装器生成器,所以它并不完全符合您的要求,但我个人认为试图智胜 vtable 并不是最理想的设计。基本上,Python 有许多不同的 C + + 包装器生成器,人们使用它们是有充分理由的——它们被记录在案,搜索结果和堆栈溢出中到处都有例子。如果您手摇一个解决方案,这是没有人见过的,这将是他们更难调试,如果他们遇到问题。即使它是封闭式的,下一个需要维护它的人也会挠头,你必须向每一个新来的人解释它。

一旦代码生成器开始工作,您所需要做的就是维护底层的 C + + 代码,而不必手动更新或修改扩展代码。(这可能离你提出的诱人的解决方案并不太远)

提出的解决方案是一个打破新引入的 派克塞尔多一点保护提供的类型安全性(当按指示使用时)的例子。

因此,尽管这样实现派生/子类可能不是最好的长期选择,但是可以将代码包装起来,让 vtable 做它最擅长的事情,当新人有问题时,你可以直接指向 随便啦 解决方案 符合 最好的的文档。

这只是我的个人观点

我在修改扩展使其与 Python3兼容时遇到了同样的问题,并且在试图解决这个问题时找到了这个页面。

我最终通过阅读 Python 解释器的源代码、 PEP 0384C-API的文档解决了这个问题。

设置 Py_TPFLAGS_HEAPTYPE标志将告诉解释器将 PyTypeObject重铸为 PyHeapTypeObjectPyHeapTypeObject包含必须分配的其他成员。在某些时候,解释器会尝试引用这些额外的成员,如果不分配这些成员,就会导致 Segfault。

Python 3.2引入了 C 结构 PyType_SlotPyType_Spec以及 C 函数 PyType_FromSpec,它们简化了动态类型的创建。简而言之,您使用 PyType_SlotPyType_Spec来指定 PyTypeObjecttp_*成员,然后调用 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分配和初始化额外内存。

希望这能有帮助。