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


Reference-Counting

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.
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.


Garbage-Collection bei zyklischen Objektbeziehungen

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.



Datentypkonversion



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 ] ...