【问题标题】:How to tidy/fix PyCXX's creation of new-style Python extension-class?如何整理/修复 PyCXX 创建的新型 Python 扩展类?
【发布时间】:2015-02-24 01:23:05
【问题描述】:

我几乎完成了对 C++ Python 包装器 (PyCXX) 的重写。

原来允许新旧样式扩展类,但也允许从新样式类派生:

import test

// ok
a = test.new_style_class();

// also ok
class Derived( test.new_style_class() ):
    def __init__( self ):
        test_funcmapper.new_style_class.__init__( self )

    def derived_func( self ):
        print( 'derived_func' )
        super().func_noargs()

    def func_noargs( self ):
        print( 'derived func_noargs' )

d = Derived()

代码很复杂,似乎包含错误 (Why does PyCXX handle new-style classes in the way it does?)

我的问题是:PyCXX 复杂机制的基本原理/理由是什么?有没有更清洁的替代品?

我将尝试在下面详细说明我在此查询中所处的位置。首先,我将尝试描述 PyCXX 目前正在做什么,然后我将描述我认为可以改进的地方。


当 Python 运行时遇到 d = Derived() 时,它会遇到 PyObject_Call( ob ) where ob is thePyTypeObjectforNewStyleClass. I will writeobasNewStyleClass_PyTypeObject`。

PyTypeObject 已经用 C++ 构建并使用 PyType_Ready 注册

PyObject_Call 将调用 type_call(PyTypeObject *type, PyObject *args, PyObject *kwds),返回一个初始化的 Derived 实例,即

PyObject* derived_instance = type_call(NewStyleClass_PyTypeObject, NULL, NULL)

类似的东西。

(所有这些都来自(http://eli.thegreenplace.net/2012/04/16/python-object-creation-sequence 顺便说一句,谢谢 Eli!)

type_call 本质上是:

type->tp_new(type, args, kwds);
type->tp_init(obj, args, kwds);

我们的 C++ 包装器已将函数插入到 NewStyleClass_PyTypeObjecttp_newtp_init 插槽中,如下所示:

typeobject.set_tp_new( extension_object_new );
typeobject.set_tp_init( extension_object_init );

:
    static PyObject* extension_object_new( PyTypeObject* subtype, 
                                              PyObject* args, PyObject* kwds )
    {
        PyObject* pyob = subtype->tp_alloc(subtype,0);

        Bridge* o = reinterpret_cast<Bridge *>( pyob );

        o->m_pycxx_object = nullptr;

        return pyob;
    }

    static int extension_object_init( PyObject* _self, 
                                            PyObject* args, PyObject* kwds )
    {
        Bridge* self{ reinterpret_cast<Bridge*>(_self) };

        // NOTE: observe this is where we invoke the constructor, 
        //       but indirectly (i.e. through final)
        self->m_pycxx_object = new FinalClass{ self, args, kwds };

        return 0;
    }

请注意,我们需要将 Python Derived 实例和它对应的 C++ 类实例绑定在一起。 (为什么?解释如下,见“X”)。为此,我们正在使用:

struct Bridge
{
    PyObject_HEAD // <-- a PyObject
    ExtObjBase* m_pycxx_object;
}

现在这座桥提出了一个问题。我很怀疑这种设计。

注意内存是如何为这个新的 PyObject 分配的:

        PyObject* pyob = subtype->tp_alloc(subtype,0);

然后我们将该指针类型转换为Bridge,并使用紧跟在PyObject 之后的4 或8 个(sizeof(void*)) 字节指向相应的C++ 类实例(这在extension_object_init 中连接为可以在上面看到)。

现在我们需要:

a) subtype-&gt;tp_alloc(subtype,0) 必须分配额外的 sizeof(void*) 字节 b) PyObject 不需要超出sizeof(PyObject_HEAD) 的任何内存,因为如果需要,那么这将与上述指针冲突

此时我的一个主要问题是: 我们能否保证 Python 运行时为我们的 derived_instance 创建的 PyObject 不会与 Bridge 的 ExtObjBase* m_pycxx_object 字段重叠?

我将尝试回答:由美国决定分配多少内存。当我们创建NewStyleClass_PyTypeObject 时,我们输入我们希望这个PyTypeObject 为这种类型的新实例分配多少内存:

template< TEMPLATE_TYPENAME FinalClass >
class ExtObjBase : public FuncMapper<FinalClass> , public ExtObjBase_noTemplate
{
protected:
    static TypeObject& typeobject()
    {
        static TypeObject* t{ nullptr };
        if( ! t )
            t = new TypeObject{ sizeof(FinalClass), typeid(FinalClass).name() };
                   /*           ^^^^^^^^^^^^^^^^^ this is the bug BTW!
                        The C++ Derived class instance never gets deposited
                        In the memory allocated by the Python runtime
                        (controlled by this parameter)

                        This value should be sizeof(Bridge) -- as pointed out
                        in the answer to the question linked above

        return *t;
    }
:
}

class TypeObject
{
private:
    PyTypeObject* table;

    // these tables fit into the main table via pointers
    PySequenceMethods*       sequence_table;
    PyMappingMethods*        mapping_table;
    PyNumberMethods*         number_table;
    PyBufferProcs*           buffer_table;

public:
    PyTypeObject* type_object() const
    {
        return table;
    }

    // NOTE: if you define one sequence method you must define all of them except the assigns

    TypeObject( size_t size_bytes, const char* default_name )
        : table{ new PyTypeObject{} }  // {} sets to 0
        , sequence_table{}
        , mapping_table{}
        , number_table{}
        , buffer_table{}
    {
        PyObject* table_as_object = reinterpret_cast<PyObject* >( table );

        *table_as_object = PyObject{ _PyObject_EXTRA_INIT  1, NULL }; 
        // ^ py_object_initializer -- NULL because type must be init'd by user

        table_as_object->ob_type = _Type_Type();

        // QQQ table->ob_size = 0;
        table->tp_name              = const_cast<char *>( default_name );
        table->tp_basicsize         = size_bytes;
        table->tp_itemsize          = 0; // sizeof(void*); // so as to store extra pointer

        table->tp_dealloc           = ...

您可以看到它以table-&gt;tp_basicsize 的形式出现

但现在我似乎很清楚,从 NewStyleClass_PyTypeObject 生成的 PyObject-s 永远不需要额外分配的内存。

这意味着整个Bridge机制是不必要的。

而 PyCXX 的原始技术使用 PyObject 作为NewStyleClassCXXClass 的基类,并初始化这个基类,以便d = Derived() 的 Python 运行时的 PyObject 实际上是这个基类,这种技术看起来不错。因为它允许无缝类型转换。

每当 Python 运行时从 NewStyleClass_PyTypeObject 调用一个槽时,它都会将一个指向 d 的 PyObject 的指针作为第一个参数传递,我们可以直接类型转换回 NewStyleClassCXXClass

所以我的问题是:为什么我们不这样做呢? NewStyleClass 派生有什么特别之处会强制为 PyObject 分配额外的空间

我意识到我不理解派生类的创建顺序。 Eli 的帖子没有涉及到这一点。

我怀疑这可能与以下事实有关

    static PyObject* extension_object_new( PyTypeObject* subtype, ...

^ 这个变量名是'subtype' 我不明白这一点,我想知道这是否是关键。

编辑:我想到了为什么 PyCXX 使用 sizeof(FinalClass) 进行初始化的一种可能解释。它可能是一个经过尝试和放弃的想法的遗物。即,如果 Python 的 tp_new 调用为 FinalClass(它以 PyObject 作为基础)分配了足够的空间,也许可以使用“placement new”或一些巧妙的 reinterpret_cast 业务在该确切位置生成一个新的 FinalClass。我的猜测是这可能已经尝试过了,发现会造成一些问题,解决了问题,然后遗物就被遗忘了。

【问题讨论】:

    标签: c++ initialization python-c-api new-style-class pycxx


    【解决方案1】:

    PyCXX 不复杂。它确实有两个错误,但无需对代码进行重大更改即可轻松修复。

    在为 Python API 创建 C++ 包装器时,会遇到问题。 C++ 对象模型和 Python 新型对象模型有很大不同。一个根本的区别是 C++ 有一个构造函数来创建和初始化对象。 Python有两个阶段; tp_new 创建对象并执行最小初始化(或仅返回现有对象),tp_init 执行其余的初始化。

    PEP 253,你应该完整阅读它,它说:

    tp_new() 槽和 tp_init() 槽之间职责的区别在于它们确保的不变量。 tp_new() 槽应该只确保最重要的不变量,否则实现对象的 C 代码将会中断。 tp_init() 槽应该用于可覆盖的用户特定初始化。以字典类型为例。该实现有一个指向永远不应该为 NULL 的哈希表的内部指针。这个不变量由字典的 tp_new() 槽处理。另一方面,字典 tp_init() 槽可用于根据传入的参数为字典提供一组初始键和值。

    ...

    您可能想知道为什么 tp_new() 插槽不应该调用 tp_init() 插槽本身。原因是在某些情况下(例如对持久对象的支持),能够创建特定类型的对象而不需要进一步初始化它是很重要的。这可以通过调用 tp_new() 槽而不调用 tp_init() 来方便地完成。也有可能没有调用 tp_init(),或者调用了多次——即使在这些异常情况下,它的操作也应该是健壮的。

    C++ 包装器的全部意义在于使您能够编写漂亮的 C++ 代码。例如,您希望您的对象具有只能在其构造期间初始化的数据成员。如果您在tp_new 期间创建对象,那么您无法在tp_init 期间重新初始化该数据成员。这可能会迫使您通过某种智能指针持有该数据成员并在tp_new 期间创建它。这使得代码很难看。

    PyCXX 采用的方法是将对象构造一分为二:

    • tp_new 创建一个虚拟对象,其中只有一个指向创建 tp_init 的 C++ 对象的指针。该指针最初为空。

    • tp_init 分配并构造实际的 C++ 对象,然后更新在tp_new 中创建的虚拟对象中的指针以指向它。如果tp_init 被多次调用,则会引发 Python 异常。

    我个人认为这种方法对我自己的应用程序的开销太高,但这是一种合法的方法。我在 Python C/API 周围有自己的 C++ 包装器,它在 tp_new 中进行所有初始化,这也是有缺陷的。似乎没有一个好的解决方案。

    【讨论】:

      【解决方案2】:

      这是一个小的 C 示例,展示了 Python 如何为从 C 类型派生的类的对象分配内存:

      typedef struct
      {
          PyObject_HEAD
          int dummy[100];
      } xxx_obj;
      

      它还需要一个类型对象:

      static PyTypeObject xxx_type = 
      {
          PyObject_HEAD_INIT(NULL)
      };
      

      还有一个初始化这个类型的模块初始化函数:

      extern "C"
      void init_xxx(void)
      {
          PyObject* m;
      
          xxx_type.tp_name = "_xxx.xxx";
          xxx_type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
      
          xxx_type.tp_new = tp_new; // IMPORTANT
          xxx_type.tp_basicsize = sizeof(xxx_obj); // IMPORTANT
      
          if (PyType_Ready(&xxx_type) < 0)
              return;
      
          m = Py_InitModule3("_xxx", NULL, "");
      
          Py_INCREF(&xxx_type);
          PyModule_AddObject(m, "xxx", (PyObject *)&xxx_type);
      }
      

      缺少的是tp_new 的实现:Python docs 要求:

      tp_new 函数应该调用subtype-&gt;tp_alloc(subtype, nitems) 为对象分配空间

      让我们这样做并添加一些打印输出。

      static
      PyObject *tp_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds)
      {
          printf("xxx.tp_new():\n\n");
      
          printf("\t subtype=%s\n", subtype->tp_name);
          printf("\t subtype->tp_base=%s\n", subtype->tp_base->tp_name);
          printf("\t subtype->tp_base->tp_base=%s\n", subtype->tp_base->tp_base->tp_name);
      
          printf("\n");
      
          printf("\t subtype->tp_basicsize=%ld\n", subtype->tp_basicsize);
          printf("\t subtype->tp_base->tp_basicsize=%ld\n", subtype->tp_base->tp_basicsize);
          printf("\t subtype->tp_base->tp_base->tp_basicsize=%ld\n", subtype->tp_base->tp_base->tp_basicsize);
      
          return subtype->tp_alloc(subtype, 0); // IMPORTANT: memory allocation is done here!
      }
      

      现在运行一个非常简单的 Python 程序来测试它。该程序创建一个从xxx 派生的新类,然后创建一个derived 类型的对象。

      import _xxx
      
      class derived(_xxx.xxx):
          def __init__(self):
              super(derived, self).__init__()
      
      d = derived()
      

      要创建派生类型的对象,Python 将调用其tp_new,而后者又将调用其基类' (xxx) tp_new。此调用生成以下输出(具体数字取决于机器架构):

      xxx.tp_new():
      
          subtype=derived
          subtype->tp_base=_xxx.xxx
          subtype->tp_base->tp_base=object
      
          subtype->tp_basicsize=432
          subtype->tp_base->tp_basicsize=416
          subtype->tp_base->tp_base->tp_basicsize=16
      

      tp_newsubtype 参数是正在创建的对象的类型 (derived),它派生自我们的 C 类型 (_xxx.xxx),而后者又派生自 object。基 object 的大小为 16,即 PyObject_HEADxxx 类型的 dummy 成员额外增加了 400 个字节,总共 416 个字节,derived Python 类增加了额外的 16 个字节.

      因为subtype-&gt;tp_basicsize 占层次结构的所有三个级别(objectxxxderived)的总大小为 432 字节,所以分配了适量的内存。

      【讨论】:

      • 啊哈!所以这意味着'd'在内存中的布局方式是:[PyObject_HEAD(16bytes)][_xxx.xxx(400 bytes)][derived(16 bytes)]。现在我终于明白为什么派生类的存在需要 PyCXX 的 Bridge 之类的东西。我明白为什么他们不得不放弃他们为旧式类使用的原始类型转换解决方案。
      • 既然您已经弄清楚了我应该首先提出的问题,如果我正确地重新提出这个问题,您是否愿意重新定位这个答案? (HERE -- 我认为这是一个宝贵的资源,没有人会从这样一个晦涩难懂的问题中找到它。对第一个答案的评论可能会重定向。
      • 多重继承会发生什么? Python 会将重复的基类解析到同一个内存空间而不是复制它吗?
      • 在 Python 中,您不能从两种完全不同的 C 类型派生。你可以从defaultdictdict派生,因为defaultdict派生自dict——就内存布局而言,它就像单独拥有defaultdcit一样——但你不能同时从dict和list`派生,因为没有办法调和它们的内存布局。
      猜你喜欢
      • 1970-01-01
      • 2011-02-04
      • 2021-11-27
      • 1970-01-01
      • 2011-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多