Как динамически создать производный тип в C-API Python - PullRequest
72 голосов
/ 09 ноября 2011

Предположим, у нас есть тип Noddy, как определено в руководстве по написанию модулей расширения C для Python . Теперь мы хотим создать производный тип, перезаписывая только метод __new__() из Noddy.

В настоящее время я использую следующий подход (проверка ошибок лишена читабельности):

PyTypeObject *BrownNoddyType =
    (PyTypeObject *)PyType_Type.tp_alloc(&PyType_Type, 0);
BrownNoddyType->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
BrownNoddyType->tp_name = "noddy.BrownNoddy";
BrownNoddyType->tp_doc = "BrownNoddy objects";
BrownNoddyType->tp_base = &NoddyType;
BrownNoddyType->tp_new = BrownNoddy_new;
PyType_Ready(BrownNoddyType);

Это работает, но я не уверен, что это правильный путь. Я бы ожидал, что мне придется также установить флаг Py_TPFLAGS_HEAPTYPE, потому что я динамически выделяю объект типа в куче, но это приводит к segfault в интерпретаторе.

Я также думал о явном вызове type(), используя PyObject_Call() или подобное, но я отказался от этой идеи. Мне нужно было бы обернуть функцию BrownNoddy_new() в объект функции Python и создать словарь, сопоставляющий __new__ с этим объектом функции, что выглядит глупо.

Как лучше всего это сделать? Мой подход правильный? Есть ли интерфейсная функция, которую я пропустил?

Обновление

В списке рассылки python-dev есть две темы по теме (1) (2) . Из этих потоков и нескольких экспериментов я делаю вывод, что не следует устанавливать Py_TPFLAGS_HEAPTYPE, если тип не назначен вызовом type(). В этих потоках есть разные рекомендации, лучше ли выделять тип вручную или вызывать type(). Я был бы счастлив с последним, если бы только знал, каков рекомендуемый способ обернуть функцию C, которая должна идти в слот tp_new. Для обычных методов этот шаг был бы легким - я мог бы просто использовать PyDescr_NewMethod(), чтобы получить подходящий объект-обертку. Я не знаю, как создать такой объект-обертку для моего __new__() метода, хотя - возможно, мне нужна недокументированная функция PyCFunction_New() для создания такого объекта-обертки.

Ответы [ 3 ]

4 голосов
/ 19 декабря 2015

Я столкнулся с той же проблемой, когда изменял расширение для совместимости с Python 3, и нашел эту страницу, когда пытался ее решить.

В конце концов я решил эту проблему, прочитав исходный код интерпретатора Python, PEP 0384 и документацию для C-API .

Установка флага Py_TPFLAGS_HEAPTYPE говорит интерпретатору преобразовать ваш PyTypeObject в PyHeapTypeObject, который содержит дополнительные члены, которые также должны быть выделены. В какой-то момент интерпретатор пытается обратиться к этим дополнительным элементам, и, если вы оставите их нераспределенными, это вызовет ошибку сегмента.

В Python 3.2 представлены структуры C PyType_Slot и PyType_Spec и функция C PyType_FromSpec, которые упрощают создание динамических типов. В двух словах, вы используете PyType_Slot и PyType_Spec, чтобы указать tp_* членов PyTypeObject, а затем вызываете PyType_FromSpec, чтобы выполнить грязную работу по выделению и инициализации памяти.

Начиная с PEP 0384, мы имеем:

typedef struct{
  int slot;    /* slot id, see below */
  void *pfunc; /* function pointer */
} PyType_Slot;

typedef struct{
  const char* name;
  int basicsize;
  int itemsize;
  int flags;
  PyType_Slot *slots; /* terminated by slot==0. */
} PyType_Spec;

PyObject* PyType_FromSpec(PyType_Spec*);

(Вышеприведенное не является буквальной копией из PEP 0384, которая также включает const char *doc в качестве члена PyType_Spec. Но этот член не указан в исходном коде.)

Чтобы использовать их в исходном примере, предположим, что у нас есть структура C BrownNoddy, которая расширяет структуру C для базового класса Noddy. Тогда мы бы получили:

PyType_Slot slots[] = {
    { Py_tp_doc, "BrownNoddy objects" },
    { Py_tp_base, &NoddyType },
    { Py_tp_new, BrownNoddy_new },
    { 0 },
};
PyType_Spec spec = { "noddy.BrownNoddy", sizeof(BrownNoddy), 0,
                      Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, slots };
PyTypeObject *BrownNoddyType = (PyTypeObject *)PyType_FromSpec(&spec);

Это должно делать все в исходном коде, включая вызов PyType_Ready, плюс все, что необходимо для создания динамического типа, включая установку Py_TPFLAGS_HEAPTYPE, а также выделение и инициализацию дополнительной памяти для PyHeapTypeObject.

Надеюсь, это полезно.

2 голосов
/ 13 января 2012

Я прошу прощения заранее, если этот ответ ужасен, но вы можете найти реализацию этой идеи в PythonQt , в частности, я думаю, что следующие файлы могут быть полезными ссылками:

Этот фрагмент из PythonQtClassWrapper_init показался мне довольно интересным:

static int PythonQtClassWrapper_init(PythonQtClassWrapper* self, PyObject* args, PyObject* kwds)
{
  // call the default type init
  if (PyType_Type.tp_init((PyObject *)self, args, kwds) < 0) {
    return -1;
  }

  // if we have no CPP class information, try our base class
  if (!self->classInfo()) {
    PyTypeObject*  superType = ((PyTypeObject *)self)->tp_base;

    if (!superType || (superType->ob_type != &PythonQtClassWrapper_Type)) {
      PyErr_Format(PyExc_TypeError, "type %s is not derived from PythonQtClassWrapper", ((PyTypeObject*)self)->tp_name);
      return -1;
    }

    // take the class info from the superType
    self->_classInfo = ((PythonQtClassWrapper*)superType)->classInfo();
  }

  return 0;
}

Стоит отметить, что PythonQt использует генератор-обертку, так что он не совсем соответствует тому, что вы просите, но лично я думаю, что попытка перехитрить vtable не самый оптимальный дизайн. По сути, существует множество различных генераторов оболочек C ++ для Python, и люди используют их по уважительной причине - они задокументированы, есть примеры, плавающие в результатах поиска и при переполнении стека. Если вы раскроете решение, которое никто не видел раньше, им будет гораздо сложнее отлаживать, если они столкнутся с проблемами. Даже если он с закрытым исходным кодом, следующий парень, который должен поддерживать его, будет чесать голову, и вам придется объяснять это каждому новому человеку, который придет.

Как только вы запустите генератор кода, все, что вам нужно сделать, это сохранить базовый код C ++, вам не нужно обновлять или модифицировать свой код расширения вручную. (Что, вероятно, не слишком далеко от заманчивого решения, с которым вы пошли)

Предлагаемое решение является примером нарушения безопасности типов, которое недавно представленный PyCapsule обеспечивает немного большую защиту от (при использовании по назначению).

Таким образом, хотя это возможно, возможно, это не лучший долгосрочный выбор для реализации производных / подклассов таким образом, а скорее обернуть код и позволить виртуальной таблице делать то, что он делает лучше всего, и когда у нового парня есть вопросы, вы можете просто указать его в документации для независимо от решения подходит лучше .

Хотя это только мое мнение. : D

1 голос
/ 09 января 2012

Один из способов понять, как это сделать, - создать его версию с помощью SWIG.Посмотрите, что он производит, и посмотрите, соответствует ли он или сделан по-другому.Из того, что я могу сказать, люди, которые пишут SWIG, имеют глубокое понимание расширения Python.Не больно видеть, как они делают вещи в любом случае.Это может помочь вам понять эту проблему.

...