Python in C einbetten (Embedding Python)


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

Übersicht: Python in C einbetten (Embedding Python)


Grundsätzliches

Bisher ging es um die Erweiterung des Python-Sprachumfangs (Extending). Das Einbetten (Embedding) befasst sich mit der entgegengesetzten Richtung der Verbindung von Python und C. In diesem Kapitel wird es darum gehen, in C Python-Funktionalität verfügbar zu machen. Dies kann insbesondere dann sinnvoll sein, wenn die Ausführungsgeschwindigkeit eine untergeordnete Rolle spielt und eine gewünschte Funktionalität in Python deutlich einfacher zu implementieren bzw. zu realisieren ist, als sie es in C wäre. Auf einfache Art und Weise lässt sich z.B. Plug-In-Funktionalität realisieren, die es einem Benutzer erlaubt bestimmte eigene Funktionen in Python zu implementieren. Diese Funktionen können im C-Programm verwendet werden ohne dieses erneut zu übersetzen. Dies wird in den Beispielen 2 und 3 genauer betrachtet werden.

Das Einbetten ist dem Erweitern zunächst einmal sehr ähnlich. Die Schnittstelle, die wir verwenden ist exakt die gleiche und auch die nötigen Schritte ähneln sich, wie folgendes Beispiel zeigt.

Python erweitern

  1. Konversion der Werte von Python- in C-Datentypen
  2. Aufruf einer C-Funktion mit den entsprechenden Werten
  3. Konversion der Daten von C- in Python-Datentypen

Python einbetten

  1. Konversion der Werte von C- in Python-Datentypen
  2. Aufruf einer Python-Funktion/Methode mit den entsprechenden Objekten
  3. Konversion der Daten von Python- in C-Datentypen
Wie sich unschwer erkennen lässt, sind die einzelnen Aufrufe und Typkonversionen nur vertauscht. Das Einbetten bringt also gar nicht so viel Neues mit sich. Der Hauptunterschied ist, dass wenn wir Python erweitern, das Hauptprogramm immer ein Python-Programm ist, während beim Erweitern das Hauptprogramm ggf. nichts mehr mit Python zu tun hat, sondern nur in Teilen Pythoncode zur Ausführung bringt.

Der erste Aufruf, den wir ausführen müssen bevor wir den Python-Interpreter verwenden können und somit Code zur Ausführung bringen, ist der Aufruf von PyInitialize(). Der Aufruf initialisiert alle globalen Datenstrukturen, allokiert den für den Interpreter benötigen Speicher und importiert das Modul __buildins__, das alle eingebauten Python-Funktionen enthält. Nach diesem Funktionsaufruf, steht der Interpreter global (also aus jedem Programmteil) zur Verfügung.



Beispiel 1: unflexible Interpretation (High-Level Embedding)

Dieses erste, noch sehr unflexible Beispiel zeigt die Ausführung von Python-Code durch Interpretation einer Stringkonstanten.
int main(int argc, char *argv[]) {

    /* Interpreter initialisieren */
    Py_Initialize();

    if (PyRun_SimpleString("from functools import *\n"
                       "def fakt(n):\n"
                       "\treturn reduce(lambda x,y: x * y, range(1, n+1)) \n"
                       "print(\"Fakultaet von 5 ist: \" + str( fakt(5) ) )") == -1)

		fprintf(stderr,"Error while interpretation\n");

    /* Interpreter beenden */
    Py_Finalize();

    return 0;
}
Zunächst wird der Interpreter initialisiert. Im Anschluss kann nun Python-Funktionalität genutzt werden, indem eine einfache Stringkonstante an die Funktion PyRun_SimpleString() übergeben wird. Der Interpreter wird den übergebenden String parsen und interpretieren. Als Funktionsresultat erhalten wir in diesem Fall 0 (kein Fehler bei der Interpretation) und die Ausgabe 120 auf der Standardausgabe.
Wie zu erkennen, besteht in diesem Fall keine Möglichkeit Rückgabewerte aus der Funktion zu erhalten oder Parameter direkt zu übergeben.


Beispiel 2: Ausfürung eines kompletten Python-Moduls (High-Level Embedding)

In diesem zweiten, etwas flexibleren Beispiel wird ein komplettes Modul verarbeitet. Allerdings lässt auch dieses Verfahren keine Interaktion zwischen C-Programm und dem Python-Skript zu.
int main(int argc, char *argv[]) {

    FILE * fact = fopen("./factorial.py","r");

    /* Interpreter initialisieren */
    Py_Initialize();

    if (PyRun_SimpleFile(fact, "./factorial.py") == -1)

		fprintf(stderr,"Error while interpretation\n");

    /* Interpreter beenden */
    Py_Finalize();

    fclose(fact);

    return 0;
}
Der Unterschied zum ersten Beispiel ist die Verarbeitung eines externen Python-Skripts ./factorial.py. Dieses Beispiel ermöglicht uns z.B. die variable Ausführung von Pythoncode ohne die C-Quelldatei(en) erneut zu übersetzen. (Stichwort Plug-In)


Beispiel 3: Interaktion zwischen C und Python - XML-Verarbeitung (Low-Level Embedding)

Die ersten beiden Beispiele geben uns zwar die Möglichkeit Pythoncode auszuführen, aber wir haben keine Möglichkeit Rückgabewerte zu nutzen, einzelne Funktionen aufzurufen oder gar Objekte zu instanzieren. Diese Möglichkeiten werden uns durch sog. lower-level-calls gegeben. Wir erhalten daduch größere Fexibilität, allerdings ist damit auch höherer Aufwand in Form von lines-of-code verbunden.
/* ... */
    Py_Initialize(); /* Initialisieren des Python-Interpreter */

    module = PyImport_Import("xmlwork"); /* Modulimport auslösen */
    if (module) {

        className = PyObject_GetAttrString(module, "Xml"); /*
        if (className && PyCallable_Check(className)) {
            /* Konstruktoraufruf Xml("addresses.xml") */

            instance = PyObject_CallFunction(className, "(s)", parmImportFile);
            if (instance) {

                /* Aufruf dict = instance.load_address_book() */
                dict = PyObject_CallMethod(instance, "load_address_book", "")

                /* ... Verzeichnis verarbeiten ... */
     }
     Py_Finalize();
/* ... */
Zunächst muss auch hier der Interpreter initialisiert werden, um überhaupt Code ausführen zu können. Im Anschluss weisen wir den Interpreter an, das Modul xmlwork.py zu importieren. Damit der Interpreter diesen Import durchführen kann, ist es zwingend erforderlich, dass das entsprechende Verzeichnis im Such-Pfad des Interpreters eingetragen ist. Dies kann ggf. durch den Aufruf von PySys_SetPath() erfolgen. Wurde das Modul erfolgreich importiert, wird nun das Objekt (in diesem Fall die Klasse) Xml der Variable className zugewiesen. Ist diese Klasse instanzierbar (PyCallable_Check()), können wir den Konstruktor mit Übergabe der zu importierenden Datei aufrufen (PyObject_CallFunction()). Wir erhalten eine Instanz der Klasse in der Variablen instance und rufen im Folgenden die Methode load_address_book auf. Hier sehen wir nun echte Interaktion, denn der Methodenaufruf liefert uns ein Dictionary zurück, das jetzt mit weiteren Schnittstellenfunktionen auf beliebige Art und Weise verarbeitet werden kann. In der Beispielanwendung wird das Dictionary mit einem Iterator durchlaufen und die Einträge der XML-Datei in einen binären Baum einfügt.

Dieses Beispiel soll verdeutlichen, dass mit wenig Code, nur durch Einbindung des Python-Interpreters schnell eine ordentliche Funktionalität erreicht werden kann. Wie bereits erwähnt bietet sich diese Technik besonders an, wenn Laufzeiteffizienz eine untergeordnete Rolle spielt und Dynamik, Flexibilität und kurze Entwicklungszeiten im Vordergrund stehen.

Eine komplette Version dieses XML-Beispiels finden Sie hier.


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