Я пишу расширения Python 3 в C++, и я пытаюсь найти способ проверить, PyObject
ли PyObject
с типом (struct), определяющим его макет экземпляра. Меня интересует только статический размер PyObject
, а не PyVarObject
. Макет экземпляра определяется структурой с определенным четко определенным макетом: обязательным заголовком PyObject
и (необязательно) определяемыми пользователем членами.
Ниже приведен пример расширения PyObject
на основе известного примера Noddy в разделе Определение новых типов:
// 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 langauge, структура NeverSeenNoddy
совместима с NoddyType
типа NoddyType
- он может вписываться в NoddyType
. Итак, отливка может быть прекрасной.
Итак, мой большой вопрос:
Есть ли какая-либо политика Python, которая может быть использована для определения совместимости PyObject
с макетом экземпляра Noddy
?
Любой способ проверить, PyObject*
ли PyObject*
на объектную часть, встроенную в Noddy
?
Если не политика, возможен ли взлом?
EDIT: Есть несколько вопросов, которые кажутся похожими, но, на мой взгляд, они отличаются от того, что я спросил. Например: доступ к базовой структуре PyObject
EDIT2: Чтобы понять, почему я назвал ответ Свена Марнаха ответом, см. Комментарии ниже этого ответа.
В Python вы можете проверить, имеет ли obj
тип Noddy
или производный тип, используя test isinstance(obj, Noddy)
. Тест в C-API ли какой-то PyObject *obj
имеет тип NoddyType
или производный тип, в основном тот же, вы используете PyObject_IsInstance()
:
PyObject_IsInstance(obj, &NoddyType)
Что касается вашего второго вопроса, нет никакого способа добиться этого, и если вы считаете, что вам это нужно, ваш дизайн имеет серьезные недостатки. Было бы лучше вывести NeverSeenNoddyType
из NoddyType
в первую очередь - тогда вышеуказанная проверка также распознает объект производного типа как экземпляр NoddyType
.
Безусловно, каждый объект начинается с PyObject_HEAD
, всегда безопасно обращаться к полям, определенным этим заголовком. Одним из полей является ob_type
(обычно доступ с использованием макроса Py_TYPE
). Если это указывает на NoddyType
или любой другой тип, полученный из NoddyType
(это то, что говорит PyObject_IsInstance
), вы можете предположить, что макет объекта - это struct Noddy
.
Другими словами, объект совместим с макетом экземпляра Noddy
если его Py_TYPE
указывает на NoddyType
или любой из его подклассов.
Во втором вопросе актерский состав не будет прекрасным. NeverSeenNoddy
Noddy
и NeverSeenNoddy
отличаются друг от друга, хотя размер может быть одинаковым.
Предполагая, что NeverSeenNoddy
является макетом типа NeverSeenNoddy_Type
, вы никогда не должны бросать в NeverSeenNoddy
если PyObject_IsInstance(obj, &NeverSeenNoddy_Type)
является ложным.
Если вы хотите иметь два типа C-уровня с общими полями, вы должны получить оба типа из общей базы, которая имеет только общие поля в макете экземпляра.
Затем подтипы должны включать базовый макет в верхней части их макетов:
struct SubNoddy {
// No PyObject_HEAD because it already in Noddy
Noddy noddy;
int extra_field;
};
Затем, если PyObject_IsInstance(obj, &SubNoddy_Type)
вернет true, вы можете SubNoddy
к SubNoddy
и получить доступ к полю extra_field
. Если PyObject_IsInstance(obj, &Noddy_Type)
возвращает true, вы можете PyObject_IsInstance(obj, &Noddy_Type)
к Noddy
и получить доступ к общим полям.
PyObject_IsInstance()
является ответом на 1). Что касается 2), мне кажется, это проблема дизайна. Если вы хотите сделать что-то подобное, убедитесь, что все участвующие типы имеют общий базовый тип. Это мой ответ на вопрос 2). Так как это не отвечает на ваш вопрос? (Обратите внимание, что я знаю, что 1) и 2) на самом деле не перечисляют вопросы, а скорее проблемные случаи в вашем посте.)