我几乎完成了重写C++ Python包装器(PyCXX)的工作。
原始类允许旧的和新的样式扩展类,但也允许从新样式类派生:
import test
// ok
a = test.new_style_class();
// also ok
class Derived( test.new_style_class() ):
def __init__( self ):
test_funcmapper.new_style_class.__init__( self )
def derived_func( self ):
print( 'derived_func' )
super().func_noargs()
def func_noargs( self ):
print( 'derived func_noargs' )
d = Derived()代码是复杂的,并且似乎包含错误(为什么PyCXX以它的方式处理新样式的类?)。
我的问题是:,PyCXX复杂机制的基本原理/理由是什么?有更清洁的选择吗?
我将试图在下面详细说明我在调查中的位置。首先,我将尝试描述PyCXX目前正在做什么,然后我将描述我认为可以改进的东西。
当Python运行时遇到PyObject_Call( ob ) where ob is thePyTypeObjectforNewStyleClass. I will writeobasNewStyleClass_PyTypeObject`.时,它会执行d = Derived()
PyTypeObject已在C++中构建并使用PyType_Ready注册
PyObject_Call将调用type_call(PyTypeObject *type, PyObject *args, PyObject *kwds),返回初始化的派生实例,即
PyObject* derived_instance = type_call(NewStyleClass_PyTypeObject, NULL, NULL)就像这样。
(所有这些都来自(顺便说一句,http://eli.thegreenplace.net/2012/04/16/python-object-creation-sequence,谢谢伊莱!)
type_call实际上是这样做的:
type->tp_new(type, args, kwds);
type->tp_init(obj, args, kwds);我们的C++包装器已经将函数插入到NewStyleClass_PyTypeObject的tp_new和tp_init插槽中,如下所示:
typeobject.set_tp_new( extension_object_new );
typeobject.set_tp_init( extension_object_init );
:
static PyObject* extension_object_new( PyTypeObject* subtype,
PyObject* args, PyObject* kwds )
{
PyObject* pyob = subtype->tp_alloc(subtype,0);
Bridge* o = reinterpret_cast<Bridge *>( pyob );
o->m_pycxx_object = nullptr;
return pyob;
}
static int extension_object_init( PyObject* _self,
PyObject* args, PyObject* kwds )
{
Bridge* self{ reinterpret_cast<Bridge*>(_self) };
// NOTE: observe this is where we invoke the constructor,
// but indirectly (i.e. through final)
self->m_pycxx_object = new FinalClass{ self, args, kwds };
return 0;
}请注意,我们需要将Python派生实例绑定在一起,它是相应的C++类实例。(为什么?解释如下,见'X')。为了做到这一点,我们使用:
struct Bridge
{
PyObject_HEAD // <-- a PyObject
ExtObjBase* m_pycxx_object;
}现在这座桥提出了一个问题。我对这个设计很怀疑。
请注意如何为这个新的PyObject分配内存:
PyObject* pyob = subtype->tp_alloc(subtype,0);然后,我们键入指向Bridge的指针,并使用PyObject后面的4或8 (sizeof(void*))字节来指向相应的C++类实例(如上面所示,在extension_object_init中连接起来)。
现在,要想让它发挥作用,我们需要:
( a) subtype->tp_alloc(subtype,0)必须分配额外的sizeof(void*)字节b) PyObject不需要sizeof(PyObject_HEAD)以外的任何内存,因为如果这样做了,这将与上面的指针相冲突
我现在面临的一个主要问题是:我们能保证PyObject运行时为derived_instance创建的derived_instance不会与桥的ExtObjBase* m_pycxx_object字段重叠吗?
我将尝试回答:这是美国决定多少内存被分配。在创建NewStyleClass_PyTypeObject时,我们需要为这种类型的新实例分配多少内存:
template< TEMPLATE_TYPENAME FinalClass >
class ExtObjBase : public FuncMapper<FinalClass> , public ExtObjBase_noTemplate
{
protected:
static TypeObject& typeobject()
{
static TypeObject* t{ nullptr };
if( ! t )
t = new TypeObject{ sizeof(FinalClass), typeid(FinalClass).name() };
/* ^^^^^^^^^^^^^^^^^ this is the bug BTW!
The C++ Derived class instance never gets deposited
In the memory allocated by the Python runtime
(controlled by this parameter)
This value should be sizeof(Bridge) -- as pointed out
in the answer to the question linked above
return *t;
}
:
}
class TypeObject
{
private:
PyTypeObject* table;
// these tables fit into the main table via pointers
PySequenceMethods* sequence_table;
PyMappingMethods* mapping_table;
PyNumberMethods* number_table;
PyBufferProcs* buffer_table;
public:
PyTypeObject* type_object() const
{
return table;
}
// NOTE: if you define one sequence method you must define all of them except the assigns
TypeObject( size_t size_bytes, const char* default_name )
: table{ new PyTypeObject{} } // {} sets to 0
, sequence_table{}
, mapping_table{}
, number_table{}
, buffer_table{}
{
PyObject* table_as_object = reinterpret_cast<PyObject* >( table );
*table_as_object = PyObject{ _PyObject_EXTRA_INIT 1, NULL };
// ^ py_object_initializer -- NULL because type must be init'd by user
table_as_object->ob_type = _Type_Type();
// QQQ table->ob_size = 0;
table->tp_name = const_cast<char *>( default_name );
table->tp_basicsize = size_bytes;
table->tp_itemsize = 0; // sizeof(void*); // so as to store extra pointer
table->tp_dealloc = ...你可以看到它以table->tp_basicsize的形式出现
但是在我看来,从NewStyleClass_PyTypeObject生成的PyObject不再需要额外的分配内存。
这意味着整个Bridge机制是不必要的。
以及PyCXX使用PyObject作为NewStyleClassCXXClass基类的原始技术,并初始化这个基础,以便Python运行时的PyObject for d = Derived()实际上就是这个基础,这种技术看起来不错。因为它允许无缝的排版。
每当Python运行时从NewStyleClass_PyTypeObject调用时隙时,它都会将指向d的PyObject的指针作为第一个参数传递,并且我们只需键入回NewStyleClassCXXClass。<-- 'X‘(上文参考)
所以,我的问题是:为什么我们不直接这么做呢?从衍生出来的东西有什么特别之处,迫使为PyObject额外分配资源吗?
我意识到,在派生类的情况下,我不理解创建序列。伊莱的帖子没有报道这件事。
我怀疑这可能与以下事实有关
static PyObject* extension_object_new( PyTypeObject* subtype, ...^这个变量名是“子类型”--我不明白这一点,我想知道这是否包含了键。
编辑:我想出了一个可能的解释来解释为什么PyCXX使用FinalClass来初始化。它可能是一个想法的遗物,被尝试和抛弃。也就是说,如果Python的tp_new调用为FinalClass (以PyObject为基础)分配了足够的空间,那么也许可以使用“placement”或一些狡猾的reinterpret_cast业务在这个确切的位置上生成一个新的FinalClass。我的猜测是,这可能是尝试过,发现了一些问题,在周围工作,并留下了遗物。
发布于 2014-12-27 03:18:32
PyCXX并不复杂。它确实有两个bug,但它们可以很容易地修复,而不需要对代码进行重大更改。
在为Python创建C++包装器时,会遇到一个问题。C++对象模型和Python对象模型非常不同。一个根本的区别是C++有一个既创建又初始化对象的构造函数。Python有两个阶段:tp_new创建对象并执行最小的intialization (或只返回现有对象),tp_init执行其余的初始化。
您可能应该完整地阅读PEP 253,它说:
tp_new()槽和tp_init()槽之间责任的区别在于它们所确保的不变量。tp_new()槽只应确保最基本的不变量,否则实现对象的C代码就会中断。tp_init()槽应该用于可覆盖的特定于用户的初始化。例如,字典类型。该实现具有一个指向哈希表的内部指针,该哈希表不应为空。这个不变量由字典的tp_new()槽来处理。另一方面,字典tp_init()槽可用于根据传入的参数为字典提供一组初始的键和值。
..。
您可能想知道为什么tp_new()槽不应该调用tp_init()槽本身。原因是在某些情况下(比如对持久对象的支持),重要的是能够创建特定类型的对象,而不对其进行任何必要的初始化。这可以通过在不调用tp_new()的情况下调用tp_init()槽来实现。还可能不调用或多次调用hat tp_init() --即使在这些异常情况下,它的操作也应该是健壮的。
C++包装器的全部目的是使您能够编写漂亮的C++代码。例如,您希望您的对象有一个只能在其构造过程中初始化的数据成员。如果在tp_new期间创建对象,则不能在tp_init期间重新初始化该数据成员。这可能会迫使您通过某种智能指针持有该数据成员,并在tp_new期间创建它。这使得代码很难看。
PyCXX采用的方法是将对象构造分为两部分:
tp_new只使用指向创建tp_init的C++对象的指针创建一个虚拟对象。这个指针最初是空的。tp_init分配并构造实际的C++对象,然后更新在tp_new中创建的虚拟对象中的指针以指向它。如果tp_init不止一次被调用,它将引发Python。我个人认为这种方法对我自己的应用程序的开销太高了,但这是一种合法的方法。我在Python /API上有自己的C++包装器,它在tp_new中完成所有初始化,这也有缺陷。似乎没有一个很好的解决办法。
发布于 2014-12-27 03:22:27
下面是一个小的C示例,它展示了Python如何为从C类型派生的类的对象分配内存:
typedef struct
{
PyObject_HEAD
int dummy[100];
} xxx_obj;它还需要一个类型对象:
static PyTypeObject xxx_type =
{
PyObject_HEAD_INIT(NULL)
};以及初始化此类型的模块初始化函数:
extern "C"
void init_xxx(void)
{
PyObject* m;
xxx_type.tp_name = "_xxx.xxx";
xxx_type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
xxx_type.tp_new = tp_new; // IMPORTANT
xxx_type.tp_basicsize = sizeof(xxx_obj); // IMPORTANT
if (PyType_Ready(&xxx_type) < 0)
return;
m = Py_InitModule3("_xxx", NULL, "");
Py_INCREF(&xxx_type);
PyModule_AddObject(m, "xxx", (PyObject *)&xxx_type);
}缺少的是tp_new的实现: Python 文档要求:
tp_new函数应该调用subtype->tp_alloc(subtype, nitems)为对象分配空间。
因此,让我们这样做,并添加一些打印。
static
PyObject *tp_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds)
{
printf("xxx.tp_new():\n\n");
printf("\t subtype=%s\n", subtype->tp_name);
printf("\t subtype->tp_base=%s\n", subtype->tp_base->tp_name);
printf("\t subtype->tp_base->tp_base=%s\n", subtype->tp_base->tp_base->tp_name);
printf("\n");
printf("\t subtype->tp_basicsize=%ld\n", subtype->tp_basicsize);
printf("\t subtype->tp_base->tp_basicsize=%ld\n", subtype->tp_base->tp_basicsize);
printf("\t subtype->tp_base->tp_base->tp_basicsize=%ld\n", subtype->tp_base->tp_base->tp_basicsize);
return subtype->tp_alloc(subtype, 0); // IMPORTANT: memory allocation is done here!
}现在运行一个非常简单的Python程序来测试它。该程序创建一个从xxx派生的新类,然后创建一个类型为derived的对象。
import _xxx
class derived(_xxx.xxx):
def __init__(self):
super(derived, self).__init__()
d = derived()要创建派生类型的对象,Python将调用它的tp_new,然后调用它的基类‘(xxx) tp_new。此调用生成以下输出(确切数字取决于机器体系结构):
xxx.tp_new():
subtype=derived
subtype->tp_base=_xxx.xxx
subtype->tp_base->tp_base=object
subtype->tp_basicsize=432
subtype->tp_base->tp_basicsize=416
subtype->tp_base->tp_base->tp_basicsize=16subtype参数用于tp_new是要创建的对象的类型(derived),它来自于我们的C类型(_xxx.xxx),后者依次来自object。基object大小为16,即PyObject_HEAD,xxx类型的dummy成员有另外400个字节,共计416字节,derived Python类增加了16个字节。
由于subtype->tp_basicsize占层次结构的所有三个级别(object、xxx和derived)的大小共计432字节,因此正在分配正确的内存量。
https://stackoverflow.com/questions/27662074
复制相似问题