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


73

假设我们具有在编写用于Python的C扩展模块Noddy教程中定义的类型。现在我们要创建一个派生类型,仅覆盖的__new__()方法Noddy

当前,我使用以下方法(去除了可读性的错误检查):

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标志,因为我在堆上动态分配了类型对象,但这样做会导致解释器出现段错误。

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

最好的方法是什么?我的方法正确吗?我错过了接口功能吗?

更新资料

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


3
据我所知,这就是这样做的方式:((但我不确定。我想是因为重写方法的要求有点特殊。)
zchenah 2011年

@CHENZhao:在我的用例中,基本类型是使用虚拟成员函数包装C ++类。派生类型只需要重写__new__()即可分配不同的C ++类。方法不需要调用,因为它们调用了虚拟成员函数。请注意,我同时使用模板技术通过完全不同的设计解决了这个问题。但是,最初的问题仍然存在。
Sven Marnach 2011年

如果在这里找不到答案,也许您可​​以询问python-dev邮件列表,然后在此处返回答案
Xavier Combelle 2011年

3
@XavierCombelle:python-dev邮件列表旨在协调Python本身的开发。这并不意味着用户提出问题。
Sven Marnach 2011年

@SvenMarnach其他可能性python-list@python.org
Xavier Combelle 2011年

Answers:


4

我在修改扩展以使其与Python 3兼容时遇到了相同的问题,并在尝试解决该问题时找到了此页面。

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

设置该Py_TPFLAGS_HEAPTYPE标志告诉解释器将您的PyTypeObjectas重铸为PyHeapTypeObject,其中还包含必须分配的其他成员。在某些时候,解释器会尝试引用这些额外的成员,并且,如果您不分配它们,将导致段错误。

Python 3.2引入了C结构PyType_SlotPyType_SpecC函数PyType_FromSpec,简化了动态类型的创建。简而言之,您可以使用PyType_SlotPyType_Spec指定的tp_*成员,PyTypeObject然后调用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为PEP 0384的成员PyType_Spec。但是该成员不会出现在源代码中。)

要在原始示例中使用它们,假设我们有一个C结构,BrownNoddy该结构扩展了基类的C结构Noddy。然后我们将有:

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,为a分配和初始化额外的内存)所需的操作PyHeapTypeObject

希望对您有所帮助。


2

如果这个答案很糟糕,我先向您道歉,但是您可以在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确实使用了包装器生成器,因此它与您的要求并不完全一致,但是我个人认为试图超越vtable并不是最佳的设计。基本上,有许多用于Python的C ++包装器生成器,人们出于充分的理由使用它们-已被记录在案,在搜索结果中和堆栈溢出时都有示例出现。如果您为此解决方案提供了前所未有的解决方案,那么如果遇到问题,调试起来将变得更加困难。即使是开源的,下一个必须维护的人也会挠头,您必须向出现的每个新人解释。

一旦代码生成器正常工作,您所需要做的就是维护底层的C ++代码,而无需手动更新或修改扩展代码。(与您使用的诱人解决方案可能相距不远)

提出的解决方案是打破类型安全性的一个示例,这种安全性是新引入的PyCapsule提供了更多的保护(按指示使用时)。

因此,虽然可能不是用这种方式实现派生/子类的最佳长期选择,而是包装代码并让vtable做到最好,而当新手有疑问时,您可以将他指向对于文档的任何 解决方案 适合 最好

不过这只是我的意见。:D


很抱歉花了这么多时间对您的答案发表评论。我还没有时间仔细浏览所有链接的QT源文件。不幸的是,我看不到您提供的示例代码如何解决我遇到的特定问题-如何为类型正确分配内存?是否可以收集动态分配的类型的垃圾?等
Sven Marnach 2012年

我确实评估了一些C ++包装器生成器-特别是SWIG,boost.python和PyCXX。尽管PyCXX的通用性最差,但它最接近我的需求,但我认为从头开始编写此代码将是我特定情况下的最佳选择。(我不会在这里详细解释我的原因。)
Sven Marnach 2012年

1

尝试并了解如何执行此操作的一种方法是使用SWIG创建其版本。查看其产生的结果,并查看其是否匹配或以其他方式完成。据我所知,一直在编写SWIG的人们对扩展Python有深入的了解。看到他们如何做事情就不会受伤。它可以帮助您了解此问题。


感谢您的回答。据我所知,SWIG不会动态生成类型,而是使用教程中描述的静态方法(请参阅我的文章开头的链接)。boost.python确实可以动态创建类型,但是由于多种原因,它使用了一种相当复杂的技术,不适用于我的情况,其中之一是我想避免使用静态变量,因为我的库是标头-只要。
Sven Marnach 2012年

嗯,是的,我完全错过了标题的动态部分。
Demolishun'1
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.