Grundsätzliche Konzepte
... [ Seminar WS2009/2010 - Programmiersprachen und -Systeme ] ... [ Python erweitern und einbetten - wie Python mit C interagiert ] ... [ Python durch C-Module erweitern ] ...
Übersicht: Grundsätzliche Konzepte
Die endliche Größe des Arbeitsspeichers macht es nötig ein Konzept zu implementieren, das nicht mehr referenziert Objekte aus dem Speicher entfernt, so dass dieser Platz
von neuen Objekten wieder belegt werden kann. Python verwendet mit dem sog. Reference-Counting einen relativ einfachen Garbage-Collection-Algorithmus.
Jedes Objekt besitzt ein Datenfeld, das die aktuelle Anzahl der Referenzen auf dieses Objekt enthät. Erreicht der Zählerstand den Wert 0, so kann der entsprechende
Destruktor aufgerufen und ausgeführt werden.
Soll in C ein Python-Objekt verwenden werden, so muss dieses Konzept adaptiert werden. Die Schnittstelle stellt dafür die beiden1 Makros Py_INCREF() sowie Py_DECREF() zur Verfügung.
Diese erlauben das Inkrementieren bzw. das Dekrementieren des Referenzzählers eines Objekts. Man kann sich vorstellen, dass Py_DECREF() das aufwendigere Makro seien muss,
da hier ein Test des Referenzzählers auf 0 durchgeführt und im entsprechenden Fall die Deallokation angestoßen werden muss.
Wichtig ist bei allen Überlegungen bezüglich des Reference-Countings klar zu stellen, dass man nie im Besitz eines Objekts ist, sondern immer nur im Besitz einer Referenz auf ein Objekt. Führt die letzte "Instanz",
die eine Referenz auf ein Objekt hält, das Makro Py_DECREF() aus, wird das Objekt aus dem Speicher entfernt. Der Besitzer einer Referenz ist immer dafür verantwortlich den Besitz wieder
abzugeben (den Referenzzähler zu dekrementieren) sobald die Referenz nicht mehr benötigt wird. Es sei darauf hingewiesen, dass Fehler beim Reference-Counting zu Speicherlecks und undefinierten Programmzuständen führen.
Um den Besitz einer Referenz abzugeben, bestehen grundsätzlich zwei Möglichkeiten. Entweder der Besitzer gibt die Referenz per Py_DECREF() frei oder er gibt den Besitz an eine andere Funktion weiter.
Somit ist die aufgerufene Funktion der neue Besitzer und damit in der Pflicht die Referenz abzugeben, wenn sie diese nicht mehr benötigt.
Wir unterscheiden prinzipiell zwischen owned-references, Referenzen die wir besitzen und unser Eigen nennen und borrowed-references, also
Referenzen die wir nur geliehen bekommen. Welche Art Referenz wir von einer Funktion erhalten oder welche Referenzart eine Funktion von uns erwartet, hängt von der entsprechenden Funktion ab. Für
Funktionen aus der Schnittstelle "Python.h" ist dies im Python/C-API Reference Manual wohl dokumentiert. Der Vorteil, den wir durch eine geborgte
Referenz erhalten ist, dass wir uns keine Gedanken um das Verringern des Referenzzählers des Objekts machen müssen, da wir nicht der Besitzer dieser Referenz sind. Die Nachteile sind allerdings ebenfalls
offensichtlich. Es treten leicht Probleme auf, wenn sich der Besitzer der geborgten Referenz zwischenzeitlich beendet bzw. die entsprechende Datenstruktur frei gibt.
In genau diesem Fall arbeiten wir mit einem undefinierten Zeiger!
Abhilfe schafft es, wenn wir für die Zeit in der wir den Zugriff auf das Objekt benötigen, den Besitz per Py_INCREF() übernehmen.
Erst wenn die Referenz nicht mehr benötigt wird, führen wir ein
Py_DECREF() durch, so dass erst dann der Speicher für das Objekt freigegeben werden kann und der verwendete Zeiger stets in einen
definierten Speicherbereich zeigt.
Wenn wir Referenzen aus Funktionsaufrufen der Python/C-API erhalten, ist es der Normalfall, dass wir eine neue Referenz auf ein Objekt erhalten.
Das bedeutet wir sind Besitzer dieser Referenz und somit für dessen Freigabe verantwortlich.
Es existieren allerdings ein paar Ausnahmen dieser Regel. Die prominentesten sind:
PyTuple_GetItem(), PyList_GetItem() und PyDict_GetItem().
Diese Funktionen liefern alle nur eine geborgte Referenz auf ein Objekt.
Bei Funktionen, bei denen Objekte z.B. in eine Datenstruktur einfügt werden, ist es i.d.R. so, dass die Funktion nur eine geborgte Referenz vom Aufrufer erhält. Der Aufrufer ist also
weiterhin Besitzer der Referenz. PyTuple_SetItem() sowie PyList_SetItem() sind Ausnahmen dieser Regel.
Diese Funktionen stehlen die Referenz (stealed reference) auf das übergebene Objekt. Funktionen, die Referenzen stehlen,
führen beim Aufruf kein Py_INCREF() durch. Wird allerdings die Struktur, z.B. die Liste, gelöscht oder ein Element aus der Struktur entfernt, so wird
auf das entsprechende Element der Liste ein Py_DECREF() durchgeführt.
Dies führt u.U. dazu, dass der Speicher für das Element freigegeben wird.
Das Problem der geborgten Referenzen mit der Folge von undefinierten Zeigern tritt nicht etwa nur in Multi-Threading-Anwendungen auf, wie man zunächst vermuten könnte. Das folgende Beispiel zeigt,
dass es auch in Single-Thread-Anwendungen zu problematischen Konstellationen kommen kann.
Problembehaftete Implementierung
void buggy_version(PyObject *list) {
PyObject *item = PyList_GetItem(list, 0);
PyList_SetItem(list, 2, PyLong_FromLong(9L)); PyObject_Print(item, stdout, 0);
}
Der Code sieht auf den ersten Blick relativ harmlos aus, allerdings ist er das nicht!
Folgendes Szenario: Der Referenzzähler aller Elemente der Liste ist 1, sodass jedes Element sobald es aus der Liste entfernt auch deallokiert wird. Zunächst erhält die Variable item eine geborgte Referenz auf das erste Element der Liste.
Im Anschluss wird das dritte Element (Index 2) der Liste überschrieben. Um das Überschreiben zu realisieren, wird PyList_SetItem() das "alte" Element aushängen, das neue einhängen und den Referenzzähler des "alten" Elements dekrementieren.
Das zu entfernende Element der Liste ist ein Objekt einer Klasse, die Zugriff auf die Liste selbst besitzt und im Destruktor das erste Element der Liste löscht. Jetzt wird das Problem sichtbar. Das Überschreiben bzw. das Löschen des dritten Listenelements
hat zur Folge, dass weiterer Code zur Ausführung kommt, der Auswirkung auf die Listenstruktur hat. Es wird nämlich genau der Speicher freigegeben, den item noch referenziert,
da die Liste die letzte Referenz auf dieses Element besaß und der Referenzzähler somit 0 wurde.
Der Aufruf von PyObject_Print() wird demnach fehlschlagen und zu einem Programmabsturz führen. Eine Visualisierung des Problems finden Sie auf der Folie 12 der Präsentationsunterlagen.
korrekte Implementierung
void none_buggy_version(PyObject *list) {
PyObject *item = PyList_GetItem(list, 0);
Py_INCREF(item);
PyList_SetItem(list, 2, PyLong_FromLong(9L)); PyObject_Print(item, stdout, 0);
Py_DECREF(item);
}
Diese Version des Algorithmus beseitigt das zuvor beschriebene Problem dadurch, dass erkannt wurde, dass das Überschreiben eines Listenelements dazu führen kann, dass zusätzlicher Code zur Ausführung kommt, der ggf. die Liste selbst verändert.
Durch das Inkrementieren des Referenzzählers von item wird zwar das erste Element der Liste entfernt und der Referenzzähler dieses Objekts dekrementiert,
der Referenzzähler ist allerdings immer noch 1, so dass der Speicher nicht freigegeben wird. Der Aufruf von PyObject_Print() ist somit weiterhin definiert.
Wird im Anschluss diese Referenz nicht mehr benötigt, so kann der Referenzzähler dekrementiert und der Speicher freigegeben werden.
1Es existieren noch zwei weitere Makros Py_XINCREF() u. Py_XDECREF(),
die zusätzlich einen Null-Zeiger-Test durchführen und nur dann den Zähler inkrementieren bzw. dekrementieren, wenn die Referenz nicht der NULL-Zeiger ist. Diese sollten allerdings nur in Ausnahmefällen verwendet werden.
Parameterübergabe und Rückgabewerte
Python verwendet bei Funktions- bzw. Methodenaufrufen das Konzept des tuple-packing. Alle Aufrufargumente werden in einem Tupel gekapselt
und es wird genau ein Parameter, nämlich dieses Tupel an die Funktion oder die Methode übergeben. Um zu gewährleisten, dass für die Zeit des Aufrufes die Elemente des Tupels
sicher, d.h. ohne Gefahr der zwischenzeitlichen Garbage-Collection, zugreifbar sind, wird vor dem Einfügen jedes Elements dessen Referenzzähler inkrementiert. Ist der Aufruf beendet wird das Tupel wieder gelöscht.
Da das Einfügen in ein Tupel die Referenzen stiehlt, wird der Referenzzähler jedes
Elements automatisch dekrementiert sobald das Tupel gelöscht wird. Daraus ergibt sich zwangsläufig, dass wenn in der aufgerufenen Funktion kein Aufruf von Py_INCREF() bzw. Py_DECREF() auf Elemente des Tupels erfolgt, alle Objekte wieder den
gleichen Referenzzählerstand haben wie vor dem Aufruf. Soll die aufgerufene Funktion längerfristig eine Referenz auf ein übergebenes Objekt speichern, so ist das Argumententupel zu destrukturieren und
die Übernahme des Besitzes der entsprechenden Referenz erforderlich. Die Methodik der Destrukturierung wird im Abschnitt Datenkonversion genauer behandelt.
Aufgrund zyklischer Beziehnungen zwischen Objekten kann es vorkommen, dass der Referenzzähler eines Objekts nie 0 und somit der Speicher für dieses Objekte
nie mehr freigegeben wird. Dieses Verhalten weist das folgende Beispiel auf.
import cStackMod
myStack = cStackMod.cStack(3)
myStack.push(myStack)
del myStack
Da durch das Einfügen des Stacks in sich selbst, dieser auch eine Referenz auf sich selbst hält, wird der Referenzzähler nie 0. Zur Lösung dieses Problems
müssen zwei weitere Funktionen (tp_traverse / tp_clear) implementiert, sowie ein zusätzliches Flag (Py_TPFLAGS_HAVE_GC) im Datentyp gesetzt werden.
Die Traversierungsfunktion durchläuft alle Elemente des zusammengesetzten Objekts (z.B. des Stacks) und überprüft ob mit diesem Objekt ein Zyklus entsteht, der fälschlicherweise verhindert, dass
das Objekt selbst freigegeben werden kann.
Die Clear-Methode ist dafür zuständig den Speicher für ein Objekt des entsprechenden Typs freizugeben. Diese kann in der Destruktor-Funktion verwendet werden.
Weitere Informationen zu cyclic garbage-collection
bzw. zur Implementierung entnehmen Sie bitte der Python-Dokumentation (Supporting cyclic garbage collection) oder dem Quellcode des cStack-Beispiels.
Konversion von C-Datentypen in Python-Objekte
Um aus einer Variablen oder einer Konstanten eines bestimmten C-Datentyps ein Python-Objekt zu erstellen, bietet die Schnittstelle eine Funktion PyObject* Py_BuildValue(const char *format, ...)
an. Dessen Aufruf erlaubt die Erzeugung eines oder ggf. mehrerer Objekte durch Angabe eines Formatstrings. Eine Auflistung der möglichen Formatstrings ist aus dem Python/C-API Reference Manual zu entnehmen.
Als weitere Argumente der Funktion folgen Variablen oder Konstanten des entsprechenden Formats. Das folgende Beispiel soll die Erzeugung einer Liste mit drei Elementen sowie die Erstellung von insgesamt sechs Objekten veranschaulichen.
static PyObject * genList(PyObject *self) {
return Py_BuildValue("[si(ss)]", "Die Antwort", 42, "i", "j");
}
Durch die Angabe der umschließenden eckigen Klammern ist definiert, das es sich bei dem zurückgegebenen Objekt um eine Liste handelt. Das erste Element der Liste wird ein Objekt vom Typ String mit dem Wert "Die Antwort" sein.
Als zweites Element folgt ein Integerobjekt mit dem Wert 42, sowie ein Tupel, das die beiden Strings "i" und "j" enthält.
Liefert die Funktion den Nullzeiger zurück, so ist eine Ausnahme aufgetreten. Nähere Informationen zum Exception-Handlung finden Sie hier.
Konversion von Python-Objekten in C-Datentypen
Selbstverständlich ist auch die inverse Konversion nötig. Bei der Konversion eines Python-Objekts in einen C-Datentyp ist zu unterscheiden ob das Objekt in einem Tupel gekapselt ist (siehe Parameterübergabe) oder ob es sich
um ein elementares Objekt handelt. Betrachten wir zunächst die Konversion des Argumententupels. Für die Destrukturierung eines Tupels stellt die Schnittstelle die Funktion int PyArg_ParseTuple(PyObject *args, const char *, ...)
zur Verfügung. Auch hier enthält das Python/C-API Reference Manual eine detailliete Beschreibung der verfügbaren Formatstrings.
Auf Parameterposition 1 muss ein Tupel-Objekt angegeben werden. Als zweites Argument folgt der entsprechende Formatstring gefolgt von Zeigern auf die Variablen in die die Werte übertragen werden sollen.
Das folgende Beispiel zeigt das Parsen eines Argumententupels mit vier Elementen:
static PyObject * parseValues(PyObject *self, PyObject *args) {
float f;
int i;
PyObject *o = NULL;
char *s = NULL;
if (PyArg_ParseTuple(args, "fiO!s", &f, &i, &PyTuple_Type, &o, &s)) {
PySys_WriteStdout("Parsen erfolgreich!\nFloat-Wert:%f\nInteger-Wert:%i\n" \
"Python-Object vom Typ: %s\nString: %s\n", f, i, o->ob_type->tp_name, s);
} else {
return NULL;
}
Py_RETURN_NONE;
}
Besonderheit in diesem Beispiel stellt die Forderung dar, dass es sich bei dem dritten Parameter zwingend um ein Tupel handeln muss. Diese Forderung entsteht durch Verwendung des Formatstrings O!. Dieser
erfordert es, vor dem Zeiger auf die Zielvariable einen Zeiger auf den entsprechenden Datentyp anzugeben. Entspricht der Übergabeparameter nicht dem geforderten Typ, so bricht die Funktion mit einer Ausnahme ab.
Für die Konversion einzelner Objekte in einen C-Datentyp stellt die Python/C-API eigene Konversionsfunktionen zur Verfügung. Zu nennen sind an dieser Stelle Funktionen der Art PyLong_*, PyFloat_*, PyBool_*, PyUnicode_*.
Da sich die Schnittstelle seit der Version 3 im Bezug auf Strings geändert hat, sind für alle Python-Versionen < 3.x die Funktionen PyString_* statt der entsprechenden Unicode-Varianten interessant.
Auch an dieser Stelle sei erneut auf das entsprechende Python-Manual verwiesen, das die detaillierte Funktionsweise im Einzelfall dokumentiert.
Behandlung von Ausnahmen (Exception-Handling)
Exceptions werden global gesetzt d.h. es existiert eine globale Variable die anzeigt ob eine Exception aufgetreten ist oder nicht (NULL). In einer zweiten
Variablen hält der Interpreter fest, welche Exception ausgelöst wurde. Eine dritte Variable hält den entsprechenden Stacktrace, wenn die Ausnahme innerhalb
des Python-Codes auftrat. Die Python/C-API definiert verschiedene Funktionen zum Setzen einer Exception.
Eine wichtige Konvention, die es dabei zu Berücksichtigen gilt ist,
dass Funktionen, die Exceptions auslösen zwingend den Nullzeiger als Funktionsergebnis zurückliefern müssen (nicht zu verwechseln mit Py_NONE). Für gewöhnlich reicht die
Verwendung der Funktion PyErr_SetString(PyObject *type, const char *message) aus. Diese erlaubt es einen Exceptiontyp sowie eine Fehlernachricht anzugeben.
Die Schnittstelle gibt bereits viele Exception-Klassen vor, wie z.B. PyExc_ZeroDivisionError, PyExc_FloatingPointError, etc...
Eine detailliertere Auflistung der Standard-Exceptions finden Sie hier.
Desweiteren ist es möglich die Funktion PyErr_SetString(PyObject *type, PyObject *value) zu nutzen um den Exceptionwert als Python-Objekt zu übergeben.
In der Regel kann vom Rückgabewert einer Funktion darauf geschlossen werden ob diese eine Exception ausgelöst hat oder nicht. Die Python/C-API bietet allerdings zusätzlich
die Funktion PyErr_Occurred() an, die ein Exceptionobjekt liefert, falls eine Exception gesetzt wurde (anderenfalls NULL).
Möchte man die Exceptionbehandlung durch den Aufrufer (auf oberster Ebene den Interpreter) verhindern, kann die Funktion PyErr_Clear() verwendet werden.
Im Falle eines Fehlers bei der Speicherallokation, ist die Funktion PyErr_NoMemory() auszuführen und dessen Rückgabewert an den Interpreter zurückzuliefern.
Für die Erzeugung eines neuen Exceptionobjekts steht die Funktion PyObject* PyErr_NewException(char *name, PyObject *base, PyObject *dict) zur Verfügung.
In den meisten Fällen wird man auf Parameterposition zwei und drei den NULL-Zeiger übergeben um direkt die Klasse Exception zu beerben. Über
den dritten Parameter wird eine Möglichkeit geschaffen der neuen Exception zusätzliche Methoden zu übergeben.
... [ Seminar WS2009/2010 - Programmiersprachen und -Systeme ] ... [ Python erweitern und einbetten - wie Python mit C interagiert ] ... [ Python durch C-Module erweitern ] ...