如何确定用于声明PyObject实例布局的结构?
我正在用C++编写Python 3的扩展,想找个办法检查一个PyObject
是否和某种类型(结构体)有关联,这种类型定义了它的实例布局。我只对固定大小的PyObject
感兴趣,不包括PyVarObject
。实例布局是由一个结构体定义的,这个结构体有一些明确的布局要求:必须有PyObject
的头部,后面可以有用户自定义的成员。
下面是一个基于著名的定义新类型的Noddy示例的PyObject
扩展的例子:
// Noddy struct specifies PyObject instance layout
struct Noddy {
PyObject_HEAD
int number;
};
// type object corresponding to Noddy instance layout
PyTypeObject NoddyType = {
PyObject_HEAD_INIT(NULL)
0, /*ob_size*/
"noddy.Noddy", /*tp_name*/
sizeof(Noddy), /*tp_basicsize*/
0, /*tp_itemsize*/
...
Noddy_new, /* tp_new */
};
需要注意的是,Noddy
是一个类型,是在编译时定义的实体,而NoddyType
是运行时内存中存在的一个对象。Noddy
和NoddyType
之间唯一明显的关系是sizeof(Noddy)
的值存储在tp_basicsize
成员中。
在Python中手动实现的继承指定了一些规则,这些规则允许在PyObject
和用于声明该特定PyObject
实例布局的类型之间进行转换:
PyObject* Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
// When a Python object is a Noddy instance,
// its PyObject* pointer can be safely cast to Noddy
Noddy *self = reinterpret_cast<Noddy*>(type->tp_alloc(type, 0));
self->number = 0; // initialise Noddy members
return reinterpret_cast<PyObject*>(self);
}
在某些情况下,比如各种槽函数中,可以安全地假设“一个Python对象就是一个Noddy”,并且可以直接转换而不需要检查。然而,有时候在其他情况下进行转换就像是盲目的转换:
void foo(PyObject* obj)
{
// How to perform safety checks?
Noddy* noddy = reinterpret_cast<Noddy*>(obj);
...
}
可以检查sizeof(Noddy) == Py_TYPE(obj)->tp_basicsize
,但这并不是一个充分的解决方案,原因有:
1) 如果用户从Noddy
派生出新类型
class BabyNoddy(Noddy):
pass
而obj
在foo
中指向的是BabyNoddy
的实例,那么Py_TYPE(obj)->tp_basicsize
就会不同。但仍然可以安全地转换为reinterpret_cast<Noddy*>(obj)
,以获取指向实例布局部分的指针。
2) 可能还有其他结构体,它们的实例布局大小和Noddy
相同:
struct NeverSeenNoddy {
PyObject_HEAD
short word1;
short word2;
};
实际上,在C语言层面上,NeverSeenNoddy
结构体与NoddyType
类型对象是兼容的——它可以适配到NoddyType
中。因此,转换可能是完全可以的。
所以,我想问的主要问题是:
有没有什么Python的规则可以用来判断一个PyObject
是否与Noddy
的实例布局兼容?
有没有办法检查PyObject*
是否指向嵌入在Noddy
中的对象部分?
如果没有规则,有没有什么技巧可以用?
编辑:有一些问题看起来类似,但在我看来它们和我问的问题是不同的。例如:访问PyObject的底层结构
编辑2:为了理解我为什么将Sven Marnach的回答标记为答案,请查看该回答下的评论。
2 个回答
因为每个对象都是从 PyObject_HEAD
开始的,所以访问这个头部定义的字段总是安全的。其中一个字段是 ob_type
(通常用 Py_TYPE
宏来访问)。如果这个字段指向 NoddyType
或任何从 NoddyType
派生的类型(这就是 PyObject_IsInstance
告诉你的),那么你可以认为这个对象的结构和 struct Noddy
是一样的。
换句话说,如果一个对象的 Py_TYPE
指向 NoddyType
或它的任何子类,那么这个对象就和 Noddy
的实例结构是兼容的。
在第二个问题中,强制转换是不可行的。虽然 Noddy
和 NeverSeenNoddy
的大小可能相同,但它们的结构是不同的。
假设 NeverSeenNoddy
是一种 NeverSeenNoddy_Type
类型的布局,如果 PyObject_IsInstance(obj, &NeverSeenNoddy_Type)
返回假,那么你绝对不应该强制转换为 NeverSeenNoddy
。
如果你想要两个 C 级别的类型有共同的字段,你应该让这两个类型都从一个只包含共同字段的基础类型派生。
然后,子类型应该在它们的布局顶部包含基础布局:
struct SubNoddy {
// No PyObject_HEAD because it's already in Noddy
Noddy noddy;
int extra_field;
};
这样,如果 PyObject_IsInstance(obj, &SubNoddy_Type)
返回真,你就可以强制转换为 SubNoddy
并访问 extra_field
字段。如果 PyObject_IsInstance(obj, &Noddy_Type)
返回真,你就可以强制转换为 Noddy
并访问共同字段。
在Python中,你可以通过使用 isinstance(obj, Noddy)
来检查 obj
是否是 Noddy
类型或者是它的子类型。对于C语言的API来说,检查一个 PyObject *obj
是否是 NoddyType
类型或其子类型的方法基本上是一样的,你可以使用 PyObject_IsInstance()
。
PyObject_IsInstance(obj, &NoddyType)
至于你的第二个问题,实际上是没有办法做到这一点。如果你觉得需要这样做,那说明你的设计存在严重的问题。最好的办法是直接让 NeverSeenNoddyType
继承自 NoddyType
,这样上面的检查就会把这个子类型的对象也识别为 NoddyType
的实例。