为什么PyCXX以它的方式处理新式类?

时间:2014-12-19 10:38:58

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

我挑选了一些C ++ Python包装器代码,允许使用者从C ++构建自定义旧样式和新样式Python类。

原始代码来自PyCXX,包含新旧样式类herehere。然而,我已经大大改写了代码,在这个问题中,我将引用我自己的代码,因为它允许我以最清晰的方式呈现我能够的情况。我认为很少有人能够在没有经过几天审查的情况下理解原始代码......对我来说,这需要几周的时间,而且我还不清楚。

旧样式只是派生自PyObject,

template<typename FinalClass>
class ExtObj_old : public ExtObjBase<FinalClass>
   // ^ which : ExtObjBase_noTemplate : PyObject    
{
public:
    // forwarding function to mitigate awkwardness retrieving static method 
    // from base type that is incomplete due to templating
    static TypeObject& typeobject() { return ExtObjBase<FinalClass>::typeobject(); }

    static void one_time_setup()
    {
        typeobject().set_tp_dealloc( [](PyObject* t) { delete (FinalClass*)(t); } );

        typeobject().supportGetattr(); // every object must support getattr

        FinalClass::setup();

        typeobject().readyType();
    }

    // every object needs getattr implemented to support methods
    Object getattr( const char* name ) override { return getattr_methods(name); }
    // ^ MARKER1

protected:
    explicit ExtObj_old()
    {
        PyObject_Init( this, typeobject().type_object() ); // MARKER2
    }

当调用one_time_setup()时,它会强制(通过访问基类typeobject())为此新类型创建关联的PyTypeObject

稍后构建实例时,它会使用PyObject_Init

到目前为止一切顺利。

但新款式使用更复杂的机器。我怀疑这与新样式类允许派生这一事实有关。

这是我的问题,为什么新的样式类处理以它的方式实现?为什么必须创建这个额外的PythonClassInstance结构?为什么它不像老式的类处理那样做事情?即只是从PyObject基类型转换?看到它没有这样做,这是否意味着它没有使用它的PyObject基类型?

这是一个很大的问题,我会继续修改这个帖子,直到我满意它才能很好地代表问题。它并不适合SO的格式,对此我很抱歉。然而,一些世界级的工程师经常访问这个网站(我之前的一个问题是由GCC的首席开发人员回答的),我非常重视吸引他们的专业知识。因此,请不要过于草率地投票结束。

新款式的一次性设置如下所示:

template<typename FinalClass>
class ExtObj_new : public ExtObjBase<FinalClass>
{
private:
    PythonClassInstance* m_class_instance;
public:
    static void one_time_setup()
    {
        TypeObject& typeobject{ ExtObjBase<FinalClass>::typeobject() };

        // these three functions are listed below
        typeobject.set_tp_new(      extension_object_new );
        typeobject.set_tp_init(     extension_object_init );
        typeobject.set_tp_dealloc(  extension_object_deallocator );

        // this should be named supportInheritance, or supportUseAsBaseType
        // old style class does not allow this
        typeobject.supportClass(); // does: table->tp_flags |= Py_TPFLAGS_BASETYPE

        typeobject.supportGetattro(); // always support get and set attr
        typeobject.supportSetattro();

        FinalClass::setup();

        // add our methods to the extension type's method table
        { ... typeobject.set_methods( /* ... */); }

        typeobject.readyType();
    }

protected:
    explicit ExtObj_new( PythonClassInstance* self, Object& args, Object& kwds )
      : m_class_instance{self}
    { }

所以新样式使用自定义的PythonClassInstance结构:

struct PythonClassInstance
{
    PyObject_HEAD
    ExtObjBase_noTemplate* m_pycxx_object;
}

PyObject_HEAD,如果我深入研究Python的object.h,只是PyObject ob_base;的一个宏 - 没有进一步的复杂性,比如#if #else。所以我不明白为什么它不能简单地:

struct PythonClassInstance
{
    PyObject ob_base;
    ExtObjBase_noTemplate* m_pycxx_object;
}

甚至:

struct PythonClassInstance : PyObject
{
    ExtObjBase_noTemplate* m_pycxx_object;
}

无论如何,似乎它的目的是将指针标记到PyObject的末尾。这将是因为Python运行时经常会触发我们放在其函数表中的函数,第一个参数将是负责调用的PyObject。所以这允许我们检索相关的C ++对象。

但我们也需要为旧式课程做到这一点。

这是负责这样做的功能:

ExtObjBase_noTemplate* getExtObjBase( PyObject* pyob )
{
    if( pyob->ob_type->tp_flags & Py_TPFLAGS_BASETYPE )
    {
        /* 
        New style class uses a PythonClassInstance to tag on an additional 
           pointer onto the end of the PyObject
        The old style class just seems to typecast the pointer back up
           to ExtObjBase_noTemplate

        ExtObjBase_noTemplate does indeed derive from PyObject
        So it should be possible to perform this typecast
        Which begs the question, why on earth does the new style class feel 
          the need to do something different?
        This looks like a really nice way to solve the problem
        */
        PythonClassInstance* instance = reinterpret_cast<PythonClassInstance*>(pyob);
        return instance->m_pycxx_object;
    }
    else
        return static_cast<ExtObjBase_noTemplate*>( pyob );
}

我的评论清楚地表达了我的困惑。

在这里,为了完整性,我们将一个lambda-trampoline插入到PyTypeObject的函数指针表中,以便Python运行时可以触发它:

table->tp_setattro = [] (PyObject* self, PyObject* name, PyObject* val) -> int
{
   try {
        ExtObjBase_noTemplate* p = getExtObjBase( self );

        return ( p -> setattro(Object{name}, Object{val}) ); 
    }
    catch( Py::Exception& ) { /* indicate error */
        return -1;
    }
};

(在本演示中,我使用tp_setattro,请注意,如果您查看PyTypeObject的文档,还有大约30个其他插槽可供查看)

(事实上,以这种方式工作的主要原因是我们可以尝试{}围绕每个蹦床捕捉{}。这样可以避免消费者编写重复的错误捕获代码。)

因此,我们为相关的C ++对象提取&#34;基类型&#34;并调用其虚拟setattro(这里仅使用setattro作为示例)。派生类将覆盖setattro,并且将调用此覆盖。

旧式课程提供了这样的覆盖,我将其标记为MARKER1 - 它位于此问题的顶部列表中。

我唯一能想到的是,不同的维护者可能使用了不同的技术。但是有一些更令人信服的理由为什么新旧风格的类需要不同的架构?


PS供参考,我应该在新的样式类中包含以下方法:

    static PyObject* extension_object_new( PyTypeObject* subtype, PyObject* args, PyObject* kwds )
    {
        PyObject* pyob = subtype->tp_alloc(subtype,0);
        PythonClassInstance* o = reinterpret_cast<PythonClassInstance *>( pyob );
        o->m_pycxx_object = nullptr;
        return pyob;
    }
对我来说,这看起来绝对错误。 它似乎是分配内存,重新转换为某些可能超出分配量的结构,然后在此结束时归零。 我很惊讶它没有引起任何崩溃。 我在源代码中的任何地方都看不到这4个字节所拥有的任何指示。

    static int extension_object_init( PyObject* _self, PyObject* _args, PyObject* _kwds )
    {
        try
        {
            Object args{_args};
            Object kwds{_kwds};

            PythonClassInstance* self{ reinterpret_cast<PythonClassInstance*>(_self) };

            if( self->m_pycxx_object )
                self->m_pycxx_object->reinit( args, kwds );
            else
                // NOTE: observe this is where we invoke the constructor, but indirectly (i.e. through final)
                self->m_pycxx_object = new FinalClass{ self, args, kwds };
        }
        catch( Exception & )
        {
            return -1;
        }
        return 0;
    }

^请注意除了默认

之外没有reinit的实现
virtual void    reinit ( Object& args  , Object& kwds    ) { 
    throw RuntimeError( "Must not call __init__ twice on this class" ); 
}


    static void extension_object_deallocator( PyObject* _self )
    {
        PythonClassInstance* self{ reinterpret_cast< PythonClassInstance* >(_self) };
        delete self->m_pycxx_object;
        _self->ob_type->tp_free( _self );
    }

编辑:感谢Yhg1s在IRC频道上的见解,我会冒险猜测。

也许是因为当你创建一个新的旧式类时,它保证它将完美地重叠PyObject结构。

因此可以安全地从PyObject派生,并将指向底层PyObject的指针传递给Python,这就是旧式类所做的(MARKER2)

另一方面,新的样式类创建了一个{PyObject + maybe something else}对象。 也就是说,做同样的技巧是不安全的,因为Python运行时最终会写出基类分配的结尾(只是一个PyObject)。

因此,我们需要让Python为类分配,并返回一个我们存储的指针。

因为我们现在不再使用PyObject基类来存储,所以我们不能使用类型转换的方便技巧来检索关联的C ++对象。 这意味着我们需要将一个额外的sizeof(void *)字节标记到实际上已经分配的PyObject的末尾,并使用它来指向我们关联的C ++对象实例。

然而,这里存在一些矛盾。

struct PythonClassInstance
{
    PyObject_HEAD
    ExtObjBase_noTemplate* m_pycxx_object;
}

^如果这确实是完成上述内容的结构,则说新样式类实例确实完全适合PyObject,即它不与m_pycxx_object重叠。

如果是这种情况,那么整个过程肯定是不必要的。

编辑:这里有一些帮助我学习必要的基础工作的链接:

http://eli.thegreenplace.net/2012/04/16/python-object-creation-sequence
http://realmike.org/blog/2010/07/18/introduction-to-new-style-classes-in-python
Create an object using Python's C API

1 个答案:

答案 0 :(得分:3)

  对我来说,这看起来绝对错误。它似乎是分配内存,重新转换为某些可能超出分配量的结构,然后在此结束时归零。我很惊讶它没有造成任何崩溃。 我在源代码中的任何地方都看不到这4个字节拥有的任何迹象

PyCXX确实分配了足够的内存,但它是偶然的。这似乎是PyCXX中的一个错误。

Python为对象分配的内存量由第一次调用PythonClass<T>的以下静态成员函数确定:

static PythonType &behaviors()
{
...
    p = new PythonType( sizeof( T ), 0, default_name );
...
}

PythonType的构造函数将python类型对象的tp_basicsize设置为sizeof(T)。这样,当Python分配一个它知道分配至少sizeof(T)个字节的对象时。它之所以有效,是因为sizeof(T)sizeof(PythonClassInstance)T派生自PythonClass<T>派生的PythonExtensionBase更大,而sizeof(PythonClassInstance)来自PythonClassInstance

然而,它忽略了这一点。它实际上应该只分配tp_init。这似乎是PyCXX中的一个错误 - 它分配了太多而不是太少的空间来存储PythonExtension<T>对象。

  

这是我的问题,为什么新的样式类处理以它的方式实现?为什么必须创建这个额外的PythonClassInstance结构?为什么不能像老式的类处理那样做事情呢?

这就是我的理论为什么新样式类与PyCXX中的旧样式类不同。

在Python 2.2之前,引入了新的样式类,类型对象中没有tp_init成员。相反,您需要编写一个构造对象的工厂函数。这就是tp_init应该如何工作 - 工厂函数将Python参数转换为C ++参数,要求Python分配内存,然后使用placement new调用构造函数。

Python 2.2添加了新的样式类和tp_init成员。 Python首先创建对象,然后调用PythonClassInstance方法。保持旧的方式将要求对象首先具有创建“空”对象的虚拟构造函数(例如,将所有成员初始化为null),然后在调用tp_init时,将具有额外的初始化阶段。这使代码变得更加丑陋。

似乎PyCXX的作者希望避免这种情况。 PyCXX首先创建一个虚拟PythonClass<T>对象,然后在调用PyObject时,使用其构造函数创建实际的PythonExtensionBase对象。

  

... 这是否意味着它没有使用其PyObject基类型

这似乎是正确的,self()基类似乎没有在任何地方使用。 m_class_instance的所有有趣方法都使用虚拟PyObject方法,该方法返回PythonClass<T>并完全忽略PythonExtensionBase基类。

我猜(但只是一个猜测)是{{1}}被添加到现有系统中,从{{1}}派生而不是清理代码似乎更容易。