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

74 投票
3 回答
6064 浏览
提问于 2025-04-17 05:57

假设我们有一个叫做 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 个回答

1

想要理解怎么做这个,可以试着用SWIG创建一个版本。看看它生成了什么,是否和你想要的结果一致,或者是用其他方式实现的。从我所了解的,写SWIG的人对如何扩展Python有很深的理解。无论如何,看看他们是怎么做的总是有帮助的,这可能会帮助你理解这个问题。

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 确实使用了一个包装生成器,所以这并不完全符合你所问的。不过我个人认为,试图绕过虚表并不是最优的设计。简单来说,有很多不同的 C++ 包装生成器可以用来和 Python 一起使用,人们使用它们是有原因的——它们有文档,有很多示例可以在搜索结果和 Stack Overflow 上找到。如果你自己手动做了一个别人没见过的解决方案,当他们遇到问题时,调试起来会更加困难。即使是闭源的,下一个需要维护它的人也会感到困惑,而你还得向每一个新来的人解释。

一旦你让代码生成器工作起来,你只需要维护底层的 C++ 代码,不需要手动更新或修改你的扩展代码。(这可能和你选择的诱人解决方案相差不远)

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

所以,虽然这样实现派生类/子类是可能的,但从长远来看,这可能不是最好的选择。更好的做法是包装代码,让虚表发挥它的作用,当新来的同事有问题时,你可以直接指给他看 相关文档,无论是 哪个解决方案,还是 最合适的 方案

不过这只是我的个人看法。 :D

5

我在修改一个扩展程序以兼容Python 3时遇到了同样的问题,后来在寻找解决办法时找到了这个页面。

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

设置Py_TPFLAGS_HEAPTYPE这个标志是告诉解释器把你的PyTypeObject重新转换为PyHeapTypeObject,后者包含一些额外的成员,这些成员也必须分配内存。如果你不分配这些额外的内存,解释器在某个时刻尝试访问这些成员时就会出错,导致程序崩溃。

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分配和初始化额外的内存。

希望这些信息对你有帮助。

撰写回答