【发布时间】:2015-02-18 06:48:39
【问题描述】:
我正在挑选一些 C++ Python 包装器代码,这些代码允许消费者从 C++ 构建自定义的旧样式和新样式 Python 类。
原始代码来自PyCXX,新旧样式类here和here。然而,我已经大量重写了代码,在这个问题中,我将参考我自己的代码,因为它使我能够以最清晰的方式呈现情况。我认为如果不经过几天的审查,几乎没有人能够理解原始代码……对我来说,这已经花费了数周时间,但我仍然不清楚。
旧式只是派生自 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