1

我正在挑选一些 C++ Python 包装器代码,这些代码允许消费者从 C++ 构建自定义的旧样式和新样式 Python 类。

原始代码来自PyCXX ,这里这里都有新旧样式类。然而,我已经大量重写了代码,在这个问题中,我将参考我自己的代码,因为它使我能够以最清晰的方式呈现情况。我认为如果没有几天的审查,很少有人能够理解原始代码......对我来说,这已经花费了数周时间,但我仍然不清楚。

旧样式只是派生自 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 + 也许是别的} 对象。ie 做同样的把戏是不安全的,因为 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
使用 Python 的 C API 创建一个对象

4

1 回答 1

3

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

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

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

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

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

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

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

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

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

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

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

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

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

我猜(不过只是一个猜测)是它PythonClass<T>被添加到现有系统中,并且似乎更容易从中派生PythonExtensionBase而不是清理代码。

于 2014-12-22T07:24:20.997 回答