Python mit C/C++ verheiraten: Python um C/C++ erweitern oder darin einbetten

Teil 1 der Serie zeigt mit Codebeispielen zum Nachbauen, wie sich C/C++-Bibliotheken direkt aus Python aufrufen lassen und wie man Erweiterungsmodule schreibt.

Lesezeit: 17 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 57 Beiträge
Schlange, Python, Paradies

(Bild: Michael Schwarzenberger, gemeinfrei (Creative Commons CC0))

Von
  • Rainer Grimm
Inhaltsverzeichnis

Es gibt viele gute Gründe, Python entweder um C/C++ zu erweitern oder in C/C++ einzubetten – grundsätzlich verstehen sich Python und C/C++ nämlich blendend. Das mit Abstand am häufigsten verwendete Python ist bereits in C implementiert und dürfte vielen geläufig sein, gemeint ist CPython. Am Anfang steht aber die Qual der Wahl: Welche Technik oder welches Framework sollte zum Einsatz kommen? Wann ist es ratsam, Python zu erweitern, wann sollte man es eher in C++ einbetten? Um diese Fragen drehen sich die beiden Artikel dieser zweiteiligen Serie zum Kombinieren der Sprachen Python und C/C++.

Bevor dieser Artikel in die Untiefen der Kommunikation zwischen Python und C/C++ abtaucht, gilt es zuerst, die elementare Frage zu klären, was es bedeutet, Python um C/C++ zu erweitern beziehungsweise Python in C/C++ einzubetten. Vereinfachend gesagt verhalten sich die beiden Techniken spiegelbildlich zueinander.

Das Erweitern geschieht in den folgenden drei Schritten:

  1. Zunächst muss man die Werte von Python nach C/C++ konvertieren.
  2. Anschließend kann man die konvertierten Werte verwenden, um die C/C++-Funktionalität auszuführen.
  3. Zuletzt lassen sich die Ergebnisse von C/C++ nach Python konvertieren.

Das Einbetten hingegen funktioniert genau umgekehrt: Zuerst werden die Werte von C/C++ nach Python konvertiert, und am Ende steht das Konvertieren der Ergebnisse von C/C++ nach Python. Das Erweitern und Einbetten von Python ist kein Selbstzweck, sondern bietet eine Reihe von Vorteilen. Der Einfachheit halber beschränkt sich folgenden Aufzählung auf drei Beispiele, welche Vorzüge die Strategie mit sich bringt.

Don't repeat yourself (DRY): Warum sollten Entwickler ein Modul neu in Python implementieren, wenn es bereits als C/C++-Bibliothek zur Verfügung steht? Es spart nicht nur viel Arbeit, sondern schont auch die Nerven. Darüber hinaus belohnt das Python-Programm seine Erstellerin oder seinen Ersteller mit einer kürzeren Ausführungszeit. C/C++-Bibliotheken lassen sich direkt aus Python aufrufen oder auch testen. Prominente Beispiele für die direkte Verwendung von C/C++-Bibliotheken aus Python sind die numerische Python-Bibliothek NumPy oder TensorFlow.

Optimierung des performanzkritischen Teils der Applikation: Wenn eine Python-Applikation ein Performanzproblem hat, findet typischerweise der folgende Dreischritt statt. Der performkritische Teil der Applikation ist zu identifizieren, in einer C/C++-Bibliothek nochmals zu implementieren und lässt sich anschließend direkt aus Python aufrufen.

Global Interpreter Lock (GIL): Soll ein rechenintensiver Job auf mehrere Schultern verteilt werden, stellt das Python vor ein großes Problem. Der GIL verhindert durch den globalen Lock, dass mehr als ein Thread in einem Python-Interpreter zugleich aktiv sein kann. Diese Einschränkung ist der Tatsache geschuldet, dass die zugrundeliegende C-Implementierung nicht Thread-sicher ist. Die Einschränkung gilt jedoch nur für die dominante Implementierung von Python in C (CPython).

Gewiss gilt es auch, einige Herausforderungen zu meistern, wenn Python und C/C++ sich fließend verstehen sollen. Zunächst speichert C/C++ seine Daten so kompakt wie möglich. Dies gilt aber nicht für Python, denn jeder Python-Datentyp ist von object abgeleitet.

Der elementare Datentyp object (Abb.1)

Der Screenshot in Abbildung 1 zeigt, dass die Instanz eines object bereits einen Satz an Attributen besitzt. Darüber hinaus hat ein object in Python 16 Bytes und ein int bereits 28 Bytes. Im Gegensatz dazu besitzt ein int in C typischerweise 4 Bytes. object ist eine Unterklasse von int[code], die spiegelbildliche Relation gilt hier deshalb nicht.

Python kennt mutable und immutable Werte. So ist zum Beispiel im Gegensatz zum C- oder C++-String der Python-String immutable. Das heißt, dass er nicht verändert werden kann. Veränderung wird in Python durch destruktive Zuweisung simuliert, Listing 1 veranschaulicht das für Python.

str = "hello"
str = str.upper()
str # hello

Listing 1: Destruktive Zuweisung in Python

In der zweiten Zeile findet die destruktive Zuweisung statt. Dabei erzeugt der Aufruf std.upper() einen neuen String, der einer Variablen mit dem gleichen Namen wie der alte String zugewiesen wird. Das bedeutet, dass der alte String implizit destruiert wird, da er nun nicht mehr referenzierbar ist. Aus Anwendersicht hat sich lediglich der Wert des Strings verändert, und der neue String wird schlicht unter dem Namen des alten "verkauft". Implizit destruiert bedeutet in diesem Fall, dass sein Referenzzähler um 1 dekrementiert und es nun die Aufgabe des automatischen Speichermanagements in Python ist, die Ressource aufzuräumen.

Jedes Objekt besitzt in Python einen Referenzzähler, der sich ähnlich wie ein [code]std::shared_ptr in C++ verhält. Besitzt der Referenzähler den Wert 0, schlägt die Stunde des automatischen Speichermanagements.

Die Möglichkeiten, zwischen Python und C/C++ zu kommunizieren, sind vielfältig. Daher wird dieser Teil der Artikelserie im Folgenden eine Auswahl an Techniken und Frameworks vorstellen.