So erstellen Sie dynamisch einen abgeleiteten Typ in der Python C-API

73

Angenommen, wir haben den Typ Noddy, der im Tutorial zum Schreiben von C-Erweiterungsmodulen für Python definiert ist . Jetzt wollen wir einen abgeleiteten Typ erstellen und nur die __new__()Methode von überschreiben Noddy.

Derzeit verwende ich den folgenden Ansatz (Fehlerprüfung auf Lesbarkeit gestrippt):

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);

Das funktioniert, aber ich bin mir nicht sicher, ob es der richtige Weg ist. Ich hätte erwartet, dass ich auch das Py_TPFLAGS_HEAPTYPEFlag setzen muss, da ich das Typobjekt auf dem Heap dynamisch zuordne, dies jedoch zu einem Segfault im Interpreter führt.

Ich habe auch darüber nachgedacht, explizit type()using PyObject_Call()oder ähnliches aufzurufen , aber ich habe die Idee verworfen. Ich müsste die Funktion BrownNoddy_new()in ein Python-Funktionsobjekt einbinden und eine Wörterbuchzuordnung __new__zu diesem Funktionsobjekt erstellen , was albern erscheint.

Was ist der beste Weg, um dies zu erreichen? Ist mein Ansatz korrekt? Gibt es eine Schnittstellenfunktion, die ich verpasst habe?

Aktualisieren

Es gibt zwei Themen zu einem verwandten Thema in der Python-Dev-Mailingliste (1) (2) . Aus diesen Threads und einigen Experimenten schließe ich, dass ich nicht festlegen sollte, es Py_TPFLAGS_HEAPTYPEsei denn, der Typ wird durch einen Aufruf an zugewiesen type(). In diesen Threads gibt es unterschiedliche Empfehlungen, ob es besser ist, den Typ manuell zuzuweisen oder aufzurufen type(). Ich würde mich über Letzteres freuen, wenn ich nur wüsste, wie die C-Funktion, die in den tp_newSteckplatz eingefügt werden soll, empfohlen wird. Für reguläre Methoden wäre dieser Schritt einfach - ich könnte nur PyDescr_NewMethod()ein geeignetes Wrapper-Objekt erhalten. Ich weiß jedoch nicht, wie ich ein solches Wrapper-Objekt für meine __new__()Methode erstellen soll - möglicherweise benötige ich die undokumentierte Funktion PyCFunction_New(), um ein solches Wrapper-Objekt zu erstellen.

Sven Marnach
quelle
3
Wie ich weiß, ist dies der
richtige
@CHENZhao: In meinem Anwendungsfall umschließt der Basistyp eine C ++ - Klasse mit virtuellen Elementfunktionen. Die abgeleiteten Typen müssen nur überschrieben werden __new__(), um eine andere C ++ - Klasse zuzuweisen. Methoden müssen nicht überschrieben werden, da sie die virtuelle Elementfunktion aufrufen. Beachten Sie, dass ich dieses Problem inzwischen durch ein völlig anderes Design mit Vorlagentechniken gelöst habe. Die ursprüngliche Frage bleibt jedoch weiterhin bestehen.
Sven Marnach
Wenn Sie hier keine Antwort finden, können Sie vielleicht die Python-Entwickler-Mailingliste fragen und mit der Antwort hierher zurückkommen
Xavier Combelle
3
@XavierCombelle: Die Python-Dev-Mailingliste soll die Entwicklung von Python selbst koordinieren. Es ist nicht für Benutzer gedacht, die Fragen stellen.
Sven Marnach
@SvenMarnach andere Möglichkeit [email protected]
Xavier Combelle

Antworten:

4

Ich habe das gleiche Problem festgestellt, als ich eine Erweiterung so geändert habe, dass sie mit Python 3 kompatibel ist, und habe diese Seite gefunden, als ich versucht habe, sie zu lösen.

Ich habe es schließlich gelöst, indem ich den Quellcode für den Python-Interpreter PEP 0384 und die Dokumentation für die C-API gelesen habe .

Durch Setzen des Py_TPFLAGS_HEAPTYPEFlags wird der Interpreter angewiesen, Ihr PyTypeObjectas neu zu formulieren PyHeapTypeObject, das zusätzliche Mitglieder enthält, die ebenfalls zugewiesen werden müssen. Irgendwann versucht der Interpreter, auf diese zusätzlichen Mitglieder zu verweisen. Wenn Sie sie nicht zuweisen, führt dies zu einem Segfault.

In Python 3.2 wurden die C-Strukturen PyType_Slotund PyType_Specdie C-Funktion eingeführt PyType_FromSpec, die die Erstellung dynamischer Typen vereinfachen. Kurz gesagt, Sie verwenden PyType_Slotund PyType_Specgeben die tp_*Mitglieder von an PyTypeObjectund rufen dann PyType_FromSpecauf, um die schmutzige Arbeit des Zuweisens und Initialisierens des Speichers zu erledigen.

Ab PEP 0384 haben wir:

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*);

(Das Obige ist keine wörtliche Kopie von PEP 0384, die auch const char *docals Mitglied von enthalten ist PyType_Spec. Dieses Mitglied wird jedoch nicht im Quellcode angezeigt.)

Um diese im ursprünglichen Beispiel zu verwenden, nehmen wir an, dass wir eine C-Struktur haben BrownNoddy, die die C-Struktur für die Basisklasse erweitert Noddy. Dann hätten wir:

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);

Dies sollte alles im ursprünglichen Code tun, einschließlich Aufrufen PyType_Ready, sowie das, was zum Erstellen eines dynamischen Typs erforderlich ist, einschließlich Einstellen Py_TPFLAGS_HEAPTYPEund Zuweisen und Initialisieren des zusätzlichen Speichers für a PyHeapTypeObject.

Ich hoffe das ist hilfreich.

Andy Wood
quelle
2

Ich entschuldige mich im Voraus , wenn diese Antwort schrecklich ist, aber Sie können eine Implementierung dieser Idee in PythonQt finden , insbesondere denke ich, dass die folgenden Dateien nützliche Referenzen sein könnten:

Dieses Fragment aus PythonQtClassWrapper_init ist für mich etwas interessant:

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;
}

Es ist erwähnenswert, dass PythonQt einen Wrapper-Generator verwendet, sodass er nicht genau Ihren Anforderungen entspricht. Ich persönlich denke jedoch, dass der Versuch, die vtable zu überlisten, nicht das optimalste Design ist. Grundsätzlich gibt es viele verschiedene C ++ - Wrapper-Generatoren für Python, die aus gutem Grund verwendet werden. Sie sind dokumentiert, es gibt Beispiele in den Suchergebnissen und beim Stapelüberlauf. Wenn Sie eine Lösung dafür bereitstellen, die noch niemand zuvor gesehen hat, ist es für sie umso schwieriger, Fehler zu beheben, wenn sie auf Probleme stoßen. Selbst wenn es sich um eine geschlossene Quelle handelt, kratzt sich der nächste, der sie warten muss, am Kopf und Sie müssen es jeder neuen Person erklären, die mitkommt.

Sobald Sie einen Codegenerator zum Laufen gebracht haben, müssen Sie nur noch den zugrunde liegenden C ++ - Code pflegen. Sie müssen Ihren Erweiterungscode nicht mehr manuell aktualisieren oder ändern. (Was wahrscheinlich nicht zu weit von der verlockenden Lösung entfernt ist, mit der Sie sich entschieden haben)

Die vorgeschlagene Lösung ist ein Beispiel für die Aufhebung der Typensicherheit , gegen die die neu eingeführte PyCapsule etwas mehr Schutz bietet (bei bestimmungsgemäßer Verwendung).

Obwohl dies möglich ist, ist es möglicherweise nicht die beste langfristige Wahl, abgeleitete / Unterklassen auf diese Weise zu implementieren, sondern den Code zu verpacken und die vtable das tun zu lassen, was sie am besten kann. Wenn der neue Mann Fragen hat, können Sie ihn einfach auf die zeigen Dokumentation für was auch immer Lösung passt am besten .

Dies ist jedoch nur meine Meinung. : D.

Synthesizerpatel
quelle
Es tut mir leid, dass Sie sich so viel Zeit genommen haben, um Ihre Antwort zu kommentieren. Ich habe noch keine Zeit gefunden, alle verknüpften QT-Quelldateien sorgfältig durchzusehen. Leider sehe ich nicht, wie der von Ihnen bereitgestellte Beispielcode mit den spezifischen Problemen umgeht, die ich hatte - wie ordne ich dem Typ Speicher richtig zu? Ist es möglich, einen dynamisch zugewiesenen Müll vom Typ sammeln zu lassen? usw.
Sven Marnach
Ich habe einige der C ++ - Wrapper-Generatoren evaluiert - insbesondere SWIG, boost.python und PyCXX. PyCXX war zwar am wenigsten vielseitig, kam aber meinen Anforderungen am nächsten, aber ich dachte mir, dass es in meiner speziellen Situation die beste Option wäre, dies selbst von Grund auf neu zu schreiben. (Ich werde meine Gründe hier nicht im Detail erklären.)
Sven Marnach
1

Eine Möglichkeit, dies zu verstehen, besteht darin, mit SWIG eine Version davon zu erstellen. Sehen Sie, was es produziert und ob es übereinstimmt oder anders gemacht wird. Nach allem, was ich sagen kann, haben die Leute, die SWIG geschrieben haben, ein tiefes Verständnis für die Erweiterung von Python. Es kann jedenfalls nicht schaden zu sehen, wie sie Dinge tun. Es kann Ihnen helfen, dieses Problem zu verstehen.

Demolishun
quelle
Danke für deine Antwort. Soweit mir bekannt ist, generiert SWIG keine Typen dynamisch, sondern verwendet den im Tutorial beschriebenen statischen Ansatz (siehe den Link am Anfang meines Beitrags). boost.python erstellt in gewisser Weise Typen dynamisch, verwendet jedoch eine ziemlich komplexe Technik, die in meinem Fall aus verschiedenen Gründen nicht anwendbar ist. Einer davon ist, dass ich statische Variablen vermeiden möchte, da meine Bibliothek ein Header ist -nur.
Sven Marnach
Ahh, ja, ich habe den dynamischen Teil des Titels total verpasst.
Demolishun