首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >如何整理/修复PyCXX创建的新风格Python扩展类?

如何整理/修复PyCXX创建的新风格Python扩展类?
EN

Stack Overflow用户
提问于 2014-12-26 21:18:19
回答 2查看 435关注 0票数 1

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

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

代码语言:javascript
复制
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()

代码是复杂的,并且似乎包含错误(为什么PyCXX以它的方式处理新样式的类?)。

我的问题是:,PyCXX复杂机制的基本原理/理由是什么?有更清洁的选择吗?

我将试图在下面详细说明我在调查中的位置。首先,我将尝试描述PyCXX目前正在做什么,然后我将描述我认为可以改进的东西。

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

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

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

代码语言:javascript
复制
PyObject* derived_instance = type_call(NewStyleClass_PyTypeObject, NULL, NULL)

就像这样。

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

type_call实际上是这样做的:

代码语言:javascript
复制
type->tp_new(type, args, kwds);
type->tp_init(obj, args, kwds);

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

代码语言:javascript
复制
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派生实例绑定在一起,它是相应的C++类实例。(为什么?解释如下,见'X')。为了做到这一点,我们使用:

代码语言:javascript
复制
struct Bridge
{
    PyObject_HEAD // <-- a PyObject
    ExtObjBase* m_pycxx_object;
}

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

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

代码语言:javascript
复制
        PyObject* pyob = subtype->tp_alloc(subtype,0);

然后,我们键入指向Bridge的指针,并使用PyObject后面的4或8 (sizeof(void*))字节来指向相应的C++类实例(如上面所示,在extension_object_init中连接起来)。

现在,要想让它发挥作用,我们需要:

( a) subtype->tp_alloc(subtype,0)必须分配额外的sizeof(void*)字节b) PyObject不需要sizeof(PyObject_HEAD)以外的任何内存,因为如果这样做了,这将与上面的指针相冲突

我现在面临的一个主要问题是:我们能保证PyObject运行时为derived_instance创建的derived_instance不会与桥的ExtObjBase* m_pycxx_object字段重叠吗?

我将尝试回答:这是美国决定多少内存被分配。在创建NewStyleClass_PyTypeObject时,我们需要为这种类型的新实例分配多少内存:

代码语言:javascript
复制
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->tp_basicsize的形式出现

但是在我看来,从NewStyleClass_PyTypeObject生成的PyObject不再需要额外的分配内存。

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

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

每当Python运行时从NewStyleClass_PyTypeObject调用时隙时,它都会将指向d的PyObject的指针作为第一个参数传递,并且我们只需键入回NewStyleClassCXXClass。<-- 'X‘(上文参考)

所以,我的问题是:为什么我们不直接这么做呢?从衍生出来的东西有什么特别之处,迫使为PyObject额外分配资源吗?

我意识到,在派生类的情况下,我不理解创建序列。伊莱的帖子没有报道这件事。

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

代码语言:javascript
复制
    static PyObject* extension_object_new( PyTypeObject* subtype, ...

^这个变量名是“子类型”--我不明白这一点,我想知道这是否包含了键。

编辑:我想出了一个可能的解释来解释为什么PyCXX使用FinalClass来初始化。它可能是一个想法的遗物,被尝试和抛弃。也就是说,如果Python的tp_new调用为FinalClass (以PyObject为基础)分配了足够的空间,那么也许可以使用“placement”或一些狡猾的reinterpret_cast业务在这个确切的位置上生成一个新的FinalClass。我的猜测是,这可能是尝试过,发现了一些问题,在周围工作,并留下了遗物。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2014-12-27 03:18:32

PyCXX并不复杂。它确实有两个bug,但它们可以很容易地修复,而不需要对代码进行重大更改。

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

您可能应该完整地阅读PEP 253,它说:

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

..。

您可能想知道为什么tp_new()槽不应该调用tp_init()槽本身。原因是在某些情况下(比如对持久对象的支持),重要的是能够创建特定类型的对象,而不对其进行任何必要的初始化。这可以通过在不调用tp_new()的情况下调用tp_init()槽来实现。还可能不调用或多次调用hat 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 /API上有自己的C++包装器,它在tp_new中完成所有初始化,这也有缺陷。似乎没有一个很好的解决办法。

票数 1
EN

Stack Overflow用户

发布于 2014-12-27 03:22:27

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

代码语言:javascript
复制
typedef struct
{
    PyObject_HEAD
    int dummy[100];
} xxx_obj;

它还需要一个类型对象:

代码语言:javascript
复制
static PyTypeObject xxx_type = 
{
    PyObject_HEAD_INIT(NULL)
};

以及初始化此类型的模块初始化函数:

代码语言:javascript
复制
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 文档要求:

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

因此,让我们这样做,并添加一些打印。

代码语言:javascript
复制
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的对象。

代码语言:javascript
复制
import _xxx

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

d = derived()

要创建派生类型的对象,Python将调用它的tp_new,然后调用它的基类‘(xxx) tp_new。此调用生成以下输出(确切数字取决于机器体系结构):

代码语言:javascript
复制
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

subtype参数用于tp_new是要创建的对象的类型(derived),它来自于我们的C类型(_xxx.xxx),后者依次来自object。基object大小为16,即PyObject_HEADxxx类型的dummy成员有另外400个字节,共计416字节,derived Python类增加了16个字节。

由于subtype->tp_basicsize占层次结构的所有三个级别(objectxxxderived)的大小共计432字节,因此正在分配正确的内存量。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/27662074

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档