如何确定用于声明PyObject实例布局的结构?

4 投票
2 回答
1012 浏览
提问于 2025-04-17 08:08

我正在用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是运行时内存中存在的一个对象。NoddyNoddyType之间唯一明显的关系是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

objfoo中指向的是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 个回答

1

因为每个对象都是从 PyObject_HEAD 开始的,所以访问这个头部定义的字段总是安全的。其中一个字段是 ob_type(通常用 Py_TYPE 宏来访问)。如果这个字段指向 NoddyType 或任何从 NoddyType 派生的类型(这就是 PyObject_IsInstance 告诉你的),那么你可以认为这个对象的结构和 struct Noddy 是一样的。

换句话说,如果一个对象的 Py_TYPE 指向 NoddyType 或它的任何子类,那么这个对象就和 Noddy 的实例结构是兼容的。

在第二个问题中,强制转换是不可行的。虽然 NoddyNeverSeenNoddy 的大小可能相同,但它们的结构是不同的。

假设 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 并访问共同字段。

5

在Python中,你可以通过使用 isinstance(obj, Noddy) 来检查 obj 是否是 Noddy 类型或者是它的子类型。对于C语言的API来说,检查一个 PyObject *obj 是否是 NoddyType 类型或其子类型的方法基本上是一样的,你可以使用 PyObject_IsInstance()

PyObject_IsInstance(obj, &NoddyType)

至于你的第二个问题,实际上是没有办法做到这一点。如果你觉得需要这样做,那说明你的设计存在严重的问题。最好的办法是直接让 NeverSeenNoddyType 继承自 NoddyType,这样上面的检查就会把这个子类型的对象也识别为 NoddyType 的实例。

撰写回答