我正在分解一些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;
}如果我深入研究Pythonobject.h,PyObject_HEAD只是PyObject ob_base;的一个宏--没有更复杂的地方,比如#if # further。所以我不明白为什么不能简单地:
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 );
}我的评论表达了我的困惑。
在这里,为了完整起见,我们在PyTypeObject的函数指针表中插入一个lambda-蹦床,以便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的,就可以看到这些插槽)
(事实上,这样做的主要原因是我们可以在每一个蹦床周围尝试{}捕捉{}}。这使使用者不必编写重复的错误捕获。)
因此,我们提取“关联的C++对象的基本类型”,并调用它的虚拟setattro (这里只使用setattro作为示例)。派生类将重写setattro,此重写将被调用。
旧式类提供了这样一种覆盖,我将其命名为MARKER1 --它在这个问题的顶部列表中。
我唯一能想到的是,也许不同的维护人员使用了不同的技术。但是,为什么新旧样式类需要不同的体系结构,还有什么更有说服力的原因吗?
作为参考,我应该包括以下来自新样式类的方法:
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;
}^注意,除了默认值之外,没有用于恢复的实现。
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 +可能是其他东西}对象。也就是说,使用同样的技巧是不安全的,因为Python运行时最终会写到基类分配结束后(这只是一个PyObject)。
因此,我们需要让Python为类分配,并返回一个我们存储的指针。
因为我们现在不再使用这个存储的PyObject基类,所以我们不能使用类型转换的方便技巧来检索相关的C++对象。这意味着我们需要在一个额外的(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
发布于 2014-12-22 07:24:20
在我看来,这看起来是完全错误的。它似乎是在分配内存,重新转换到某个可能超过分配量的结构,然后在该结构的末尾将其作废。我很惊讶它没有造成任何车祸。--我在源代码中看不到任何迹象表明这4个字节是属于的
PyCXX确实分配了足够的内存,但这是偶然的。这似乎是PyCXX中的一个bug。
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)。这似乎是PyCXX中的一个bug --它分配的空间太多,而不是存储PythonClassInstance对象的空间太少。
这就是我的问题,为什么新样式的类处理会以这样的方式实现?为什么要创建这个额外的PythonClassInstance结构?为什么它不能像旧式类处理那样做事情呢?
下面是我的理论,为什么新样式类不同于PyCXX中的旧样式类。
在Python2.2之前,引入了新的样式类,在类型对象中没有tp_init成员。相反,您需要编写一个工厂函数来构造对象。这就是PythonExtension<T>应该如何工作的方式--工厂函数将Python参数转换为C++参数,要求Python分配内存,然后使用placement调用构造函数。
Python2.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派生出来,而不是清理代码。
https://stackoverflow.com/questions/27564257
复制相似问题