如何在 Python C-API 中动态创建派生类型
Posted
技术标签:
【中文标题】如何在 Python C-API 中动态创建派生类型【英文标题】:How to dynamically create a derived type in the Python C-API 【发布时间】:2011-12-25 08:54:02 【问题描述】:假设我们有Noddy
中定义的tutorial on writing C extension modules for Python 类型。现在我们要创建一个派生类型,只覆盖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) 上有两个相关主题的主题。从这些线程和一些实验中,我推断我不应该设置Py_TPFLAGS_HEAPTYPE
,除非该类型是通过调用type()
分配的。这些线程中有不同的建议,无论是手动分配类型还是调用type()
更好。如果我知道包装应该放在 tp_new
插槽中的 C 函数的推荐方法是什么,我会对后者感到满意。对于常规方法,这一步很容易——我可以使用PyDescr_NewMethod()
来获取合适的包装对象。我不知道如何为我的__new__()
方法创建这样的包装对象——也许我需要未记录的函数PyCFunction_New()
来创建这样的包装对象。
【问题讨论】:
据我所知,这就是这样做的方法:(但我不确定。我认为是这样,因为覆盖 new 方法的要求很友好奇特的.. @CHENZhao:在我的用例中,基本类型是用虚拟成员函数包装 C++ 类。派生类型只需覆盖__new__()
即可分配不同的C++ 类。方法不需要被覆盖,因为它们调用了虚拟成员函数。请注意,我同时通过使用模板技术的完全不同的设计解决了这个问题。不过,最初的问题仍然存在。
如果你在这里找不到答案,也许你可以询问 python-dev 邮件列表,然后返回这里来回答
@XavierCombelle:python-dev 邮件列表旨在协调 Python 本身的开发。它不适用于提问的用户。
@SvenMarnach 其他可能性 python-list@python.org
【参考方案1】:
我在修改扩展以兼容 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 的文字副本,其中还包括 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
分配和初始化额外内存。
我希望这会有所帮助。
【讨论】:
【参考方案2】:如果这个答案很糟糕,我很抱歉,但你可以在PythonQt 中找到这个想法的实现,特别是我认为以下文件可能是有用的参考:
PythonQtClassInfo.cpp PythonQtClassInfo.h PythonQtClassWrapper.cpp PythonQtClassWrapper.h来自 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 确实使用了包装器生成器,因此它并不完全符合您的要求,但我个人认为试图超越 vtable 并不是最优化的设计。基本上,Python 有许多不同的 C++ 包装器生成器,人们使用它们是有充分理由的——它们被记录在案,搜索结果和堆栈溢出中都有一些示例。如果您为此手动推出以前没有人见过的解决方案,那么如果他们遇到问题,他们将很难进行调试。即使它是封闭源代码,下一个必须维护它的人也会摸不着头脑,你必须向每一个出现的新人解释它。
一旦代码生成器开始工作,您只需维护底层 C++ 代码,无需手动更新或修改扩展代码。 (这可能与您采用的诱人解决方案相距不远)
建议的解决方案是打破新引入的PyCapsule 提供的a bit more protection 所针对的类型安全性的一个示例(按指示使用时)。
因此,尽管以这种方式实现派生类/子类可能不是最佳的长期选择,而是包装代码并让 vtable 做它最擅长的事情,当新人有问题时,你可以指出他在whateversolutionfitsbest的文档中。
这只是我的意见。 :D
【讨论】:
很抱歉花这么多时间来评论您的答案。我还没有时间仔细查看所有链接的 QT 源文件。不幸的是,我看不到您提供的示例代码如何处理我遇到的特定问题——我如何为该类型正确分配内存?是否可以收集动态分配的类型垃圾?等 我确实评估了一些 C++ 包装器生成器——尤其是 SWIG、boost.python 和 PyCXX。虽然功能最少,PyCXX 最接近我的需要,但我认为自己从头开始编写将是我特定情况下的最佳选择。 (这里我不会详细解释我的原因。)【参考方案3】:尝试了解如何执行此操作的一种方法是使用 SWIG 创建它的一个版本。看看它产生了什么,看看它是否匹配或以不同的方式完成。据我所知,编写 SWIG 的人对扩展 Python 有深入的了解。无论如何,看看他们是如何做事的,不会有什么坏处。它可以帮助你理解这个问题。
【讨论】:
感谢您的回答。据我所知,SWIG 不会动态生成类型,而是使用教程中描述的静态方法(请参阅我帖子开头的链接)。 boost.python 在某种程度上确实动态创建类型,但它使用了一种相当复杂的技术,由于各种原因不适用于我的情况,其中一个原因是我想避免使用静态变量,因为我的库是头文件-仅。 啊,是的,我完全错过了标题的动态部分。以上是关于如何在 Python C-API 中动态创建派生类型的主要内容,如果未能解决你的问题,请参考以下文章