Я почти закончил переписывать оболочку 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 встречает d = Derived()
, оно делает PyObject_Call( ob ) where ob is the
PyTypeObjectfor
NewStyleClass. I will write
О.Б.as
NewStyleClass_PyTypeObject`.
Этот 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 ++ вставила функции в tp_new
а также tp_init
слоты NewStyleClass_PyTypeObject
что-то вроде этого:
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 Derived и его соответствующий экземпляр класса C ++. (Почему? Объяснено ниже, см. «Х»). Для этого мы используем:
struct Bridge
{
PyObject_HEAD // <-- a PyObject
ExtObjBase* m_pycxx_object;
}
Теперь этот мост поднимает вопрос. Я очень подозрительно отношусь к этому дизайну.
Обратите внимание, как память была выделена для этого нового PyObject:
PyObject* pyob = subtype->tp_alloc(subtype,0);
И затем мы приведем этот указатель к Bridge
и используйте 4 или 8 (sizeof(void*)
байт сразу после PyObject
указать на соответствующий экземпляр класса C ++ (это подключается в extension_object_init
как видно выше).
Теперь, чтобы это работало, нам нужно:
а) subtype->tp_alloc(subtype,0)
должен выделять дополнительный sizeof(void*)
байтов
б) PyObject
не требует никакой памяти за sizeof(PyObject_HEAD)
, потому что если бы это было так, то это будет конфликтовать с указателем выше
На данный момент у меня есть один важный вопрос:
Можем ли мы гарантировать, что PyObject
что среда исполнения Python создала для нашего derived_instance
не пересекается с бриджем ExtObjBase* m_pycxx_object
поле?
Я попытаюсь ответить на него: именно США определяют, сколько памяти будет выделено. Когда мы создаем NewStyleClass_PyTypeObject
мы кормим, сколько памяти мы хотим, чтобы это 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
Но теперь мне кажется, что PyObject-ы генерируются из NewStyleClass_PyTypeObject
никогда не потребует дополнительной выделенной памяти.
Что означает, что это целое Bridge
Механизм не нужен.
И оригинальный метод PyCXX для использования PyObject в качестве базового класса NewStyleClassCXXClass
и инициализировать эту базу так, чтобы PyObject среды выполнения Python для d = Derived()
на самом деле эта база, эта техника выглядит хорошо. Потому что это позволяет бесшовную типизацию.
Всякий раз, когда среда выполнения Python вызывает слот из NewStyleClass_PyTypeObject
, он будет передавать указатель на PyObject d в качестве первого параметра, и мы можем просто ввести обратно NewStyleClassCXXClass
, <- ‘X’ (ссылка выше)
Поэтому на самом деле мой вопрос: почему бы нам просто не сделать это? Есть ли что-то особенное в выводе из NewStyleClass
что вызывает дополнительное выделение для PyObject?
Я понимаю, что не понимаю последовательность создания в случае производного класса. Пост Элая не охватывал это.
Я подозреваю, что это может быть связано с тем, что
static PyObject* extension_object_new( PyTypeObject* subtype, ...
^ это имя переменной ‘подтип’
Я не понимаю этого, и мне интересно, может ли это держать ключ.
РЕДАКТИРОВАТЬ: я подумал об одном возможном объяснении того, почему PyCXX использует sizeof (FinalClass) для инициализации. Это может быть пережитком идеи, которую попробовали и отбросили. Т.е. если вызов Python tp_new выделяет достаточно места для FinalClass (в качестве базы которого используется PyObject), возможно, новый FinalClass может быть сгенерирован в этом точном месте с использованием ‘размещения нового’ или некоторого хитрого бизнеса reinterpret_cast. Я предполагаю, что это можно было попытаться найти, создать какую-то проблему, обойти, и реликвия осталась позади.
PyCXX не запутан. У него есть две ошибки, но их можно легко исправить, не требуя значительных изменений в коде.
При создании оболочки C ++ для Python API возникает проблема. Объектная модель C ++ и объектная модель нового стиля Python сильно различаются. Одно принципиальное отличие состоит в том, что в C ++ есть единственный конструктор, который создает и инициализирует объект. Пока Python имеет два этапа; tp_new
создает объект и выполняет минимальную инициализацию (или просто возвращает существующий объект) и tp_init
выполняет остальную часть инициализации.
ОПТОСОЗ 253, который вы, вероятно, должны прочитать полностью, говорит:
Разница в обязанностях между слотом tp_new () и слотом tp_init () заключается в инвариантах, которые они обеспечивают. Слот tp_new () должен обеспечивать только самые существенные инварианты, без которых код C, который реализует объекты, сломался бы. Слот tp_init () должен использоваться для перезаписываемых пользовательских инициализаций. Взять, к примеру, тип словаря. Реализация имеет внутренний указатель на хеш-таблицу, которая никогда не должна быть NULL. Об этом инварианте заботится слот tp_new () для словарей. Слот словаря tp_init (), с другой стороны, можно использовать для предоставления словарю начального набора ключей и значений на основе переданных аргументов.
…
Вы можете задаться вопросом, почему слот tp_new () не должен вызывать сам слот tp_init (). Причина в том, что в определенных обстоятельствах (например, поддержка постоянных объектов) важно иметь возможность создавать объект определенного типа, не инициализируя его дальше, чем это необходимо. Это удобно сделать, вызвав слот tp_new () без вызова tp_init (). Также возможно, что tp_init () не вызывается или вызывается более одного раза — его работа должна быть надежной даже в этих аномальных случаях.
Весь смысл оболочки C ++ состоит в том, чтобы дать вам возможность написать хороший код на C ++. Скажем, например, что вы хотите, чтобы у вашего объекта был элемент данных, который можно инициализировать только во время его создания. Если вы создаете объект во время tp_new
то вы не сможете повторно инициализировать этот элемент данных во время tp_init
, Это, вероятно, заставит вас удерживать этот элемент данных с помощью умного указателя и создавать его во время tp_new
, Это делает код ужасным.
Подход PyCXX состоит в том, чтобы разделить конструкцию объекта на две части:
tp_new
создает фиктивный объект только с указателем на объект C ++, который создается tp_init
, Этот указатель изначально нулевой.
tp_init
выделяет и конструирует фактический объект C ++, затем обновляет указатель в фиктивном объекте, созданном в tp_new
указать на это. Если tp_init
вызывается более одного раза и вызывает исключение Python.
Я лично считаю, что издержки такого подхода для моих собственных приложений слишком высоки, но это законный подход. У меня есть собственная оболочка C ++ вокруг Python C / API, которая выполняет всю инициализацию в tp_new
, что тоже ошибочно. Кажется, для этого нет хорошего решения.
Вот небольшой пример 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
: Питон документы требуют, чтобы:
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=16
subtype
аргумент tp_new
тип создаваемого объекта (derived
), это происходит от нашего типа C (_xxx.xxx
), который в свою очередь происходит от object
, База object
имеет размер 16, который просто PyObject_HEAD
, xxx
тип имеет дополнительные 400 байтов для его dummy
член в общей сложности 416 байтов и derived
Класс Python добавляет дополнительные 16 байтов.
Так как subtype->tp_basicsize
учитывает размеры всех трех уровней иерархии (object
, xxx
а также derived
) в общей сложности 432 байта выделяется правильный объем памяти.