【问题标题】:Why does PyCXX handle new-style classes in the way it does?为什么 PyCXX 会以它的方式处理新式类?
【发布时间】:2015-02-18 06:48:39
【问题描述】:

我正在挑选一些 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,请注意还有大约 30 个其他插槽,您可以查看 PyTypeObject 的文档)

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

因此,我们提取“关联 C++ 对象的基本类型”并调用它的虚拟 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 );
    }

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

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

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

另一方面,新样式类创建了一个 {PyObject + 也许是别的} 对象。 即,做同样的把戏是不安全的,因为 Python 运行时最终会写到基类分配的末尾(这只是一个 PyObject)。

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

因为我们现在不再使用 PyObject 基类进行存储,所以我们不能使用类型转换的便捷技巧来检索关联的 C++ 对象。 这意味着我们需要在实际分配的 PyObject 末尾标记一个额外的 sizeof(void*) 字节,并使用它来指向我们关联的 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

【问题讨论】:

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


    【解决方案1】:

    对我来说,这看起来完全错误。它似乎正在分配内存,重新转换到某些可能超过分配数量的结构,然后在此结束时清空。我很惊讶它没有导致任何崩溃。 我在源代码的任何地方都看不到这 4 个字节的所有权

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

    Python 为对象分配的内存量由第一次调用PythonClass&lt;T&gt; 的以下静态成员函数决定:

    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&lt;T&gt;PythonExtensionBase 派生自PythonExtensionBase,它足够大)。

    但是,它没有抓住重点。它实际上应该只分配 sizeof(PythonClassInstance) 。这似乎是 PyCXX 中的一个错误 - 它分配了太多而不是太少的空间来存储 PythonClassInstance 对象。

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

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

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

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

    PyCXX 的作者似乎想避免这种情况。 PyCXX 首先创建一个虚拟的PythonClassInstance 对象,然后在调用tp_init 时,使用其构造函数创建实际的PythonClass&lt;T&gt; 对象。

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

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

    我猜(不过只是一个猜测)是 PythonClass&lt;T&gt; 已添加到现有系统中,而且似乎更容易从 PythonExtensionBase 派生而不是清理代码。

    【讨论】:

    猜你喜欢
    • 2016-07-18
    • 2020-04-07
    • 2012-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多