如何在 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_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 的文字副本,其中还包括 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 中动态创建派生类型的主要内容,如果未能解决你的问题,请参考以下文章

如何从基类动态创建派生类

如何从基类动态创建和使用派生类?

来自 Python 的 C-API - 如何获取字符串?

如何在派生类上动态调用静态方法

python的类

MFC自己派生的CButton类如何添加鼠标单击事件响应函数