Python durch C-Module erweitern (Extending Python)


... [ Seminar WS2009/2010 - Programmiersprachen und -Systeme ] ... [ Python erweitern und einbetten - wie Python mit C interagiert ] ... [ Python in C einbetten ] ...

Übersicht: Python durch C-Module erweitern (Extending Python)


Problemstellung

Dieser Abschnitt soll in ein komplexeres Beispiel einführen, in dem es darum geht die bisher eher theoretischen Ansätze in ein reales Extension-Modul umzusetzen. Die Aufgabe wird es sein einen Datentyp bzw. eine Klasse für einen FILO-Speicher (First-In-Last-Out) also einen Stack zu entwickeln.

Datentypen

Konstruktoren

Operationen

Vorbedingungen für Operationsausführung



Definition der internen Datenstruktur

Zunächst ist anhand der Problemstellung eine adäquate Datenstruktur zu entwerfen, die zwingend das Makro PyObject_HEAD enthalten muss. Durch diese Definition erhält die Objektdatenstruktur ein Feld zur Speicherung des Referenzzählers sowie des Zeigers auf seine Datentypdefinition. An dieses Makro können sich nun die benutzerspezifischen Elemente anschließen. Die Spezifikation schreibt die Speicherung von size-Elementen sowie die Stackgröße vor. Da die Verwaltung der Stackelemente in einem Array erfolgt, wird zusätzlich der aktuelle Füllstand des Arrays im Objekt gespeichert. Die entsprechende Struktur sieht wie folgt aus:
typedef struct {

    PyObject_HEAD /* contains ref-count and typeinformation */
    unsigned int maxSize;
    unsigned int len /* number of elements on stack */;
    PyObject ** e /* array of size elements */;
	
} StackObject;



Definition des Datentypen cStack

Ein zentraler Bestandteil der Modulentwicklung ist die Definition des Datentypen. Die ausführliche Struktur ist dem Kapital Grundlagen / Der Datentyp "PyTypeObject" zu entnehmen. Die folgende Typdefinition ist nicht vollständig und enthält nur die verwendeten Elemente.
/**
 * Typeinformation-Structure for the StackObject
 */
static PyTypeObject Stack_Type = {

    PyVarObject_HEAD_INIT(NULL, 0)
    "cStack", /*tp_name*/
    sizeof (StackObject), /*tp_basicsize*/
    sizeof (PyObject*), /*tp_itemsize*/
    /* methods */
    (destructor) Stack_dealloc, /*tp_dealloc*/ 
    (traverseproc) Stack_traverse, /*tp_traverse*/
    (inquiry) Stack_clear, /*tp_clear*/
    (initproc) Stack_init, /*tp_init*/
    (newfunc) Stack_new, /*tp_new*/
    "cStack Type", /*tp_doc*/
    Stack_methods, /*tp_methods*/
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */ /* allows us to subclass this type. cyclic-gc is included also */
	
};
Wichtig ist es, analog zur Objektdefinition auch wieder eine Standarddefinition einzubinden. Hier ist es PyVarObject_HEAD_INIT(). Dass hier zunächst der NULL-Zeiger und eine 0 übergeben wird, besagt das dieser Typ von keiner Basisklasse abgeleitet wird1. Die Felder tp_basesize sowie tp_itemsize geben an wieviel Speicher im Konstruktor für das Objekt sowie für jedes Stackelement zu allokieren ist. Desweiteren ist zu erwähnen, dass es erst durch die Nennung des Flags Py_TPFLAGS_BASETYPE möglich wird, später diesen Datentypen zu beerben. Im Folgenden wird nun auf die Struktur des im Feld tp_methods angegebenen Arrays eingegangen.

1Durch Aufruf der Funktion PyType_Ready(&Stack_Type) aus der PyMODINIT-Function wird später Vererbung simuliert, indem Zeiger von zu erbenden Funktionen in die Struktur einkopiert werden.


Bekanntgabe von Methoden

Welche Methodenaufrufe für einen Datentyp definiert sind und wie sich die Parameterübergabe gestaltet, wird durch ein Array der Struktur PyMethodDef festgelegt. Dieses wird wie zuvor gesehen in der entsprechenden Typdefinition referenziert. Es handelt sich dabei um eine simple Datenstruktur bestehend aus vier Feldern. Das erste Feld beschreibt den Methodennamen. Dieser referenziert den als zweiten Parameter genannte C-Funktionszeiger. Es findet also ein Mapping von Methodenname auf C-Funktion statt. Der dritte Wert bezieht sich auf die Parameterübergabe. Sinnvolle Werte in diesem Kontext sind METH_VARARGS, METH_NOARGS und METH_KEYWORDS. Die Verwendung von METH_VARARGS führt dazu, dass der Interpreter beim Funktionsaufruf ein automatisches tuple-packing durchführt. Es wird also (außer dem Objekt selbst) nur ein Parameter an die Funktion übergeben. Dieser Parameter muss wie bereits beschrieben durch entsprechende Funktionen destrukturiert werden. Verwendet man das Flag METH_NOARGS so wird die Übergabe von Argumenten an die so markierte Funktion verboten. Da es sich bei diesen Konstanten um Bitmasken handelt, lassen sich z.B. durch eine strikte Oder-Verknüpfung die beiden Werte METH_VARARGS und METH_KEYWORDS verknüpfen. Somit ist die Übergabe als Key/Value-Pair, sowie die Übergabe auf expliziter Parameterposition möglich. Ist METH_KEYWORDS angegeben, so wird der Funktion ein zusätzlicher Parameter (Dictionary) übergeben, der die Schlüssel/Wert-Paare enthält. Für das Parsen dieser Parameter steht die Funktion PyArg_ParseTupleAndKeywords() zur Verfügung. Die Möglichkeit der Übergabe als Schlüssel/Wert-Paar ist dem folgenden Beispiel zu entnehmen.
static
PyObject * parseKwds(PyObject *self, PyObject *args, PyObject * keywds) {

    char *name = NULL;
    int age = 45;

    float weight = 91.8;

    static char *kwlist[] = {"name", "alter", "gewicht", NULL};
     /* all arguments after | are optional so we should assign default values in the declaration-part */
    if (PyArg_ParseTupleAndKeywords(args, keywds, "s|if", kwlist, &name, &age, &weight)) {
        PySys_WriteStdout("Parsen erfolgreich!\nName:%s\nAlter:%i\n" \
			  "Gewicht: %f\n", name, age, weight);
    } else {

        return NULL;
    }

    Py_RETURN_NONE;
}


Die Moduldefinition

Um das Erweiterungsmodul zu spezifizieren, ist die Struktur PyModuleDef zu verwenden. Diese Struktur beinhaltet zunächst die Angabe eines Basismoduls (in diesem Fall NULL). Es folgen der Modulname, sowie ein Dokumentationsstring, der über das Attribut __doc__ des Moduls aus Python heraus abgerufen werden kann. Die nächste Angabe bezieht sich auf den beim Import zusätzlich zu allokierenden Speicher. Da dieses Modul beim Import keinen zusätzlichen Speicherplatz benötigt, wird dies durch den Wert -1 kenntlich gemacht. Das folgende Feld erwartet eine Struktur wie bereits bei der Bekanntgabe von Methoden gesehen. Eine Hinterlegung abweichend von NULL erlaubt es nach dem Import des Moduls alle in der spezifizierten Struktur enthaltenen Funktionen auszuführen (ohne Erzeugung eines Objekts). Da wir in diesem Modul eine Fabrikmethode mkEmpty5() zur Verfügung stellen möchten, deren Aufruf in der Struktur Stack_factory_methods definiert ist, wird diese hier entsprechend angegeben. Die letzten vier Werte sind mit NULL belegt, da keine speziellen Funktionen beim Entfernen des Moduls aus dem Interpreter benötigt werden. Die komplette Struktur ist die folgende:
/* Module structure */
static struct PyModuleDef Stackmodule = {

    PyModuleDef_HEAD_INIT, /* common module information */
    "cStackMod", /* module name */
    module_doc, /* documentantion */
    -1, /* additional memory allocation --> none*/
    Stack_factory_methods, /* availible functions */
    NULL, NULL, NULL, NULL /* special dealloc operations */

};


Das Modul initialisieren

Wenn ein Python-Programm unser Modul zum ersten Mal importiert, wird automatisch die Funktion PyInit_[MODULNAME] aufgerufen, wobei in diesem Beispiel als Modulname "cStackMod" einzusetzen ist. Es ist zwingend vorgeschrieben eine Funktion mit diesem Namensmuster zu implementieren.
/* Initialisierung des Moduls cStackMod */
PyMODINIT_FUNC PyInit_cStackMod(void) {

   PyObject *m = NULL;
   /* Nachbildung der Vererbung -> Alles von Object erben */
   if (PyType_Ready(&Stack_Type) == 0) { /* 0 -> alles ok! */

      m = PyModule_Create(&Stackmodule);
      if (m) {

         Py_INCREF(&Stack_Type);
         PyModule_AddObject(m, Stack_Type.tp_name, 
                           (PyObject*) &Stack_Type);
      }
   }

      return m;
}
Zunächst wird die Funktion PyType_Ready() aufgerufen. Diese Funktion bildet Vererbung nach und komplettiert somit unsere Klasse. Ähnlich wie in der Programmiersprache Java, ist auch in Python jede Klasse, falls keine explizite Angabe einer Basisklasse erfolgt, eine direkte Subklasse der Klasse object. Da wir in unserer Typdefinition keine Basisklasse angegeben haben, "erben" wir demnach alle von uns nicht explizit definierten Funktionen der Klasse object. Jetzt wird das Modul durch den Funktionsaufruf von PyModule_Create() erzeugt und dem Interpreter zur Verfügung gestellt. Welche Methoden das Modul enthält wird durch die entsprechende Eintragung der Struktur PyModuleDef bestimmt (siehe
Die Moduldefinition). Konnte das Modul erstellt werden, so wird diesem jetzt der Datentyp "cStack" hinzugefügt. Da auch der Datentyp selbst ein Objekt ist, inkrementieren wir dessen Referenzzähler und fügen ihn durch Aufruf der Funktion PyModule_AddObject() dem Modul hinzu. Hierbei sei darauf hingewiesen, dass diese Funktion die Referenz auf den Datentyp stiehlt. Somit wird gewährleistet, dass durch das Entfernen des Moduls aus dem Interpreter auch das entsprechende Typ-Objekt entfernt wird.


Beispiel: Der neue Datentyp cStack

Den kompletten Quellcode dieses Beispiels finden Sie hier.


Erzeugung von Erweiterungsmodulen mit Unterstützung des Python-Moduls "distutils"

Um das komplette Modul kompilieren zu können und in Python zugänglich zu machen, bedienen wir uns eines Python-Moduls namens distutils. Hier finden Sie ein kleines Setup-Skript, welches das Stack-Modul kompiliert und damit verfügbar macht. Um das Modul nutzen zu können, muss es im Anschluss entweder im aktuellen Arbeitsverzeichnis abgelegt, aus einem speziellen Pfad importiert oder in den Python-Import-Pfad übernommen werden.


Subclassing cStack

Da wir in der Datentypdefinition im Feld tp_flags das Flag Py_TPFLAGS_BASETYPE gesetzt haben, ist es uns nun möglich diese Klasse zu beerben, also eine Subklasse der Klasse cStack zu implementieren. Dies wird im folgenden Beispiel getan.
from cStackMod import *
class extStack(cStack):
    def __init__(self, arg):
        cStack.__init__(self,arg)

    def isEmpty(self):
        return self.getSize() == 0

    def pop(self):
        return cStack.pop(self) * 10

def testExtStack():
    myExtStack = extStack(5)
    print("Stack (nach Instanzierung) ist leer? " + str(myExtStack.isEmpty()))

    for i in range(1,6):
        myExtStack.push(i)
    print("Stack (nach Einfuegen) ist leer? " + str(myExtStack.isEmpty()))
    for i in range(1,6):
        print(myExtStack.pop())

    print("Stack (nach Leeren) ist leer? " + str(myExtStack.isEmpty()))

    del myExtStack

Das Beispiel implementiert eine neue Klasse extStack auf Basis des bekannten Stacks. Es wird ein zusätzliches Prädikat isEmpty() definiert sowie die Methode pop() überschrieben. Außerdem ist eine Funktion testExtStack implementiert, die die neue Funktionalität testet und entsprechende Ausgaben produziert.


Abschließende Bemerkungen

Das Vorgehen, wie es hier beschrieben ist, eignet sich im Speziellen für die Neuentwicklung eines Moduls, das exklusiv als Pythonmodul zur Verfügung stehen muss. Nun wird dies sicher nicht immer der Fall sein. Möglicherweise möchte man bestehende Teile oder ganze Quelldateien nur zusätzlich zur nativen Ausführung auch in Python verwenden können. In diesem Fall ist dieser Ansatz, der ausschließlich in Zusammenarbeit mit Python funktioniert, nicht durchführbar. Für diesen Zweck wird es sinnvoll sein, die bestehenden Funktionen zu wrappen, also einzuwickeln. Um eine Funktion einzuwickeln, wird man sich eine zusätzliche Funktion schreiben, die Argumente von Python entgegennimmt, diese entsprechend destrukturiert und an die bestehende Funktion als C-Datentypen weitergibt. Aus dem berechneten Ergebnis wird vor der Rückgabe an den Aufrufer wieder ein Python-Objekt erstellt. Es existieren einige Tools, die dem Entwickler in diesem Zusammenhang viel Arbeit abnehmen. Das wohl bekannteste und flexibelste ist das Werkzeug
"SWIG", das sich nicht nur für die Anbindung von Python, sondern auch für eine Reihe weiterer Sprachen eignet.

Alle nötigen Dateien für die Übersetzung und anschließende Nutzung des Stack-Beispiels sind in folgendem Archiv enthalten: download


... [ Seminar WS2009/2010 - Programmiersprachen und -Systeme ] ... [ Python erweitern und einbetten - wie Python mit C interagiert ] ... [ Python in C einbetten ] ...