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)
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
cStack, PySequence, PyObject, Integer
Konstruktoren
cStack(int size) - Konstruktion eines leeren Stacks der Größe size
cStack(PySequence elements) - Konstruktion eines Stacks mit allen Elementen der Sequenz
Operationen
PyNone push(PyObject e) - Element e auf den Stack legen
PyObject pop() - Das oberste Element des Stacks zurückliefern und entfernen
PyObject top() - Wie pop() nur ohne das Element zu löschen
int getSize() - Liefert die Anzahl der Stackelemente
Vorbedingungen für Operationsausführung
top(): cStack.getSize() > 0
pop(): cStack.getSize() > 0
push(): cStack.getSize() < size
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
unsigned int maxSize;
unsigned int len;
PyObject ** e;
} StackObject;
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.
static PyTypeObject Stack_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"cStack",
sizeof (StackObject),
sizeof (PyObject*),
(destructor) Stack_dealloc,
(traverseproc) Stack_traverse,
(inquiry) Stack_clear,
(initproc) Stack_init,
(newfunc) Stack_new,
"cStack Type",
Stack_methods,
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
};
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.
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};
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;
}
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:
static struct PyModuleDef Stackmodule = {
PyModuleDef_HEAD_INIT,
"cStackMod",
module_doc,
-1,
Stack_factory_methods,
NULL, NULL, NULL, NULL
};
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.
PyMODINIT_FUNC PyInit_cStackMod(void) {
PyObject *m = NULL;
if (PyType_Ready(&Stack_Type) == 0) {
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.
Den kompletten Quellcode dieses Beispiels finden Sie hier.
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.
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 ] ...