en / de
AI
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

C++ und Python mit Pybind11

Python mit C++

In diesem Blogbeitrag wird die Integration von C++-Modulen in Python behandelt, wobei der Fokus auf der Erstellung von Binding-Code liegt. Zunächst werden die grundlegenden Unterschiede zwischen den beiden Programmiersprachen erläutert, um ein besseres Verständnis für die Herausforderungen und Möglichkeiten der Interoperabilität zu schaffen. Anschließend erfolgt eine Einführung in die Bibliothek Pybind11, die eine effiziente und benutzerfreundliche Möglichkeit bietet, C++-Funktionen und -Klassen in Python verfügbar zu machen. Anhand von anschaulichen Codebeispielen wird demonstriert, wie Binding-Code erstellt wird und welche Aspekte dabei zu beachten sind. Der Beitrag richtet sich an Entwickler, die ihre Python-Anwendungen durch leistungsstarke C++-Module erweitern möchten, und bietet praktische Tipps sowie Best Practices für die Implementierung.

Python vs. C++

Bevor wir die Python Bindings näher anschauen, werden hier kurz einige Konzepte/Prinzipien von C++ und Python erläutert.

Marshalling

Marshalling beschreibt das Konzept, wie Datentypen zwischen der Python und der C++ Welt konvertiert werden. Dies ist notwendig, da Python die Daten anders abspeichert als C++. Zum Beispiel ist in C++ ein uint32 immer 32-Bit gross. In Python ist alles vom Typ Object.  Ein Integer belegt mehrere Bytes und die Anzahl ist abhängig von der Version von Python oder auch vom Betriebssystem. Im Wesentlichen heisst das, dass beispielsweise jeder C++ Integer in einen Python Integer umgewandelt werden muss.

Mutable/Immutable Objekte

Pythons Konzept von Immutable und Mutable Objekten bringt gewisse Restriktionen. Beispielsweise ist eine Liste in Python mutable:

# Beispiel Mutable
is_mutable = ['x', 'y', 'z']
is_mutable[2] = 'a' # Variable ist veränderbar

Andererseits ist ein Tuple immutable:

# Beispiel Immutable
is_not_mutable = ('x', 'y', 'z')
is_not_mutable[2] = 'a' # Erzeugt Fehler beim Ausführen!

Daher ist Vorsicht geboten, wenn eine C++ Funktion eine Referenz als Parameter hat und man diese im Python Code benutzen möchte.

Memory Managing

C++ behandelt das Memory Managing anders als Python. In Python gibt es den Garbage Collector, der die nicht mehr benötigten Speicherallozierungen automatisch löscht. In C++ muss dies jedoch der Entwickler selber erledigen. Dies sieht voraus, dass der Entwickler bei der Erstellung der Python-Bindings weiss, welche Seite für die Allozierung von Speicher verantwortlich ist und diese gegebenenfalls selbst wieder frei gibt.

Pybind11

Gründe für die Verwendung von C++ mit Python

Im wesentlichen existieren 3 Gründe, wo man die Verwendung von C++ mit Python gezielt haben möchte:

  1. Man hat schon eine ausführlich getestete C++ Bibliothek, welche man wiederverwenden möchte
  2. Optimierung der Performance, wo gewisse Teile der Software in C++ ausgeführt werden sollten
  3. Man möchte Test-Tools von Python für das Projekt verwenden

Es gibt etliche Tools, welche die Erstellung von Python-Bindings ermöglichen. Dieser Blog wird sich mit Pybind11 befassen. Pybind11 hat die Spezialität, dass es nur C++11 oder höher unterstützt und ausserdem eine Header-Only Library ist. Dies macht das Kompillieren einfacher, da man nicht gegen zusätzliche Libraries linken muss.

Einfaches Beispiel mit einer Funktion

Als erstes machen wir ein einfaches Beispiel mit einer Funktion. Die C++ Datei hat folgenden Inhalt:

// pybindExample.cpp
#include <pybind11/pybind11.h>


int multiply(int x, int y) {
    return x*y;
}

PYBIND11_MODULE(pybindExample, m) {
    m.doc() = "pybind11 example "; // Optionaler String für Dokumentation

    m.def("multiply", &multiply, "Funktion die 2 Zahlen multipliziert");
}

Zeilen 6-8 beschreiben die Implementation der C++ Funktion. In diesem Fall werden die beiden übergebenen Parameter multipliziert und das Resultat zurück gegeben.

Zeile 10 ist ein Makro, welches eine Funktion kreiert, die dann in Python aufgerufen werden kann. Der erste Parameter ist der Name des Moduls. Wichtig zu beachten ist hier, dass der Name nicht als String übergeben wird. Der zweite Parameter (hier m) ist eine Variable vom Typ py::module_, welche das Interface für das Binding bildet. Innerhalb dieses Makros werden dann alle zu bindenden Funktionen, Klassen, Variablen ect. definiert. Der Aufruf von m.doc() ist optional und kann als Dokumentation des Moduls verwendet werden. Zeile 13 generiert sogenannten Binding Code, welcher die Funktion multiply() für Python erstellt. Der erste Parameter ist der Name der Funktion, welche dann in Python verwendet werden kann. Es ist nicht zwingend, dass der Name der Gleiche sein muss wie jener der C++ Funktion. Der zweite Parameter ist die Adresse der C++ Funktion und der dritte Parameter ist wieder optional für die Dokumentation der Funktion.

Nach dem Builden entsteht ein .pyd Modul (hier pybindExample.pyd), welches dann so in Python verwendet werden kann:

import pybindExample as pyExample


def example():
    print(pyExample.multiply(2, 3))


if __name__ == "__main__":
    example()

Zeile 1 zeigt den Import des Moduls mit dem vorhin gewählten Namen pybindExample. Auf Zeile 4 wird dann der Funktionsaufruf mit pybindExample.multiply(2, 3) gemacht.

Keyword Arguments

Mit diesem Beispiel können auch Keyword Argumente hinzugefügt werden. Dies verbessert die Lesbarkeit des Funktionsaufrufes besonders bei Funktionen mit vielen Parametern:

#include <pybind11/pybind11.h>

int subtract(int x, int y) {
    return x - y;
}

namespace py = pybind11;

PYBIND11_MODULE(pybindExample, m) {
    m.def("subtract", &subtract, "Function for subtraction", py::arg("x"), py::arg("y"));
}

In Zeile 10 wird der Aufruf für die subtract-Methode gemacht. Nun können als zusätzliche Parameter die Keyword Argumente mitgegeben werden (Parameter 4 und 5). Sie sind vom Typ py::arg. Nach dem Kompilieren kann das in Python die Funktion wiefolgt benutzt werden:

import pybindExample as m


def test_main():
    print(m.subtract(5, 2)) # Printet 3
    print(m.subtract(y=5, x=2)) # Vertauscht die Reihenfolge dank Keyword Argumenten, die man angeben kann. Printet -3


if __name__ == "__main__":
    test_main()

Durch das Hinzufügen der Keyword Argumente werden diese auch in der Dokumentation der Funktion angezeigt.

Default Argumente

Zusätzlich können Default Argumente wie folgt angegeben werden:

#include <pybind11/pybind11.h> 

int subtract(int x = 5, int y = 2) { 
    return x - y; } 

namespace py = pybind11; 

PYBIND11_MODULE(pybindExample, m) { 
    m.def("subtract", &subtract, "Function for subtraction", py::arg("x") = 5, py::arg("y") = 2);
}

Die Default Werte werden nicht gleich bei der Definition der Funktion auf Zeile 3 übernommen. Diese müssen nochmals angegeben werden, wie dies auf Zeile 9 sichtbar ist. Dabei muss aufgepasst werden, dass man an beiden Orten die gleichen Werte nimmt. Die Default Werte werden dann auch automatisch in der Dokumentation ersichtlich.

Variablen

Natürlich können auch Variablen so gebunden werden:

PYBIND11_MODULE(pybindExample, m) {
    m.attr("simple_var") = 100;
    m.attr("stringVal") = "Hello World";
}

In Python erfolgt der Aufruf dann so:

import pybindExample as m


def test_main():
    print(m.simple_var) # Printet 100
    print(m.stringVal) # Printet "Hello World"


if __name__ == "__main__":
    test_main()

Strukturen/Klassen

Es besteht auch die Möglichkeit, Bindings für Strukturen/Klassen zu erstellen. Hier ein Beispiel:

struct Person {
    Person(const std::string &name) : name(name) { }
    void setName(const std::string &newName) { name = newName; }
    const std::string &getName() const { return name; }

    std::string name;
};

Zur Erstellung vom Binding wird dann folgendes benötigt:

#include <pybind11/pybind11.h>

namespace py = pybind11;

PYBIND11_MODULE(pybindExample, m) {
    py::class_<Person>(m, "Person")
        .def(py::init<const std::string &>())
        .def("setName", &Person::setName)
        .def("getName", &Person::getName);
}

Auf Zeile 6 wird py::class_ verwendet um Bindings für Klassen und Structs zu erzeugen. Zeile 7 zeigt eine Funktion, welche die Parameter als Template Argumente für den Konstruktor nimmt und diese verpackt, Zeile 8 und 9 zeigen die Funktionen.

In Python kann das Struct dann so verwendet werden:

import pybindExample as m


def test_main():
    p = m.Person("Harry") # Erzeugt Python Object
    print(p.getName()) # Printet "Harry"
    p.setName("James") # Setzt neuen Namen "James"
    print(p.getName()) # Printet neuen Namen "James"
    print(p) # Printet "<cmake_example.Person object at 0x000001F085A8FCF0>
 
if __name__ == "__main__": 
    test_main()

Wie zu erkennen ist, kann das Struct problemlos erzeugt und die Funktionen verwendet werden. Wenn man die Variable aber printet, dann bekommt man nur die Adresse des Objekts im Speicher. Pybind bietet eine Möglichkeit, sogenannte dunder-Methods (hier beispielsweise __repr__), welche speziell in Python existieren, zu implementieren. Dies macht den Aufruf in Zeile 9 viel leserlicher. Dazu muss im C++ Code folgendes angepasst werden:

py::class_<Person>(m, "Person")
    .def(py::init<const std::string &>()) 
    .def("setName", &Person::setName) 
    .def("getName", &Person::getName)
    .def("__repr__", 
        [](const Person &x) {
            return "<pybindExample.Person named " + x.name + ">";
        });

Nun wird beim Aufruf von print(p) der Name anstatt die Adresse geprintet.

Bis jetzt ist es nur möglich mit der Setter und Getter Methode den Namen zu ändern. Möchte man nun direkt auf die Variable name zugreifen, muss im C++ Code folgendes ergänzt werden:

py::class_<Person>(m, "Person") 
    .def(py::init<const std::string &>()) 
    .def_readwrite("name", &Person::name) // Macht die Variable name les- und schreibbar
    // Getter und Setter ect.
    );

Somit ist es dann möglich in Python direkt die Variable zu lesen/schreiben:

p = pybindExample.Person("John")
print(p.name) # Printet: John
p.name = "Anthony"
print(p.name) # Printet: Anthony

Dynamische Attribute in Klassen

Python unterstützt die Funktionalität, dass Klassen dynamisch neue Attribute zugewiesen werden können:

class Person:
    name = "John"

p = Person()
p.name = "Carl" # Überschreiben der Variable name
p.last_name = "Wellington" # Dynamisch hinzugefügtes Attribut

Default mässig können C++ Klassen dies nicht. Pybind11 bietet aber die Möglichkeit, dass diese Klassen dazu befähigt werden. Dafür braucht es den sogenannted py::dynamic_attr Tag, welchen man im py::class_ Konstruktor hinzufügt:

py::class_<Person>(m, "Person", py::dynamic_attr())
    .def(py::init<>())
    .def_readwrite("name", &Person::name);

Dies macht es nun möglich, Attribute in Python dynamisch hinzuzufügen:

p = pybindExample.Person()
p.name = "James" # Nichts neues, Variable name wird überschrieben
p.last_name = "Jameson" # Neues Attribut last_name zum Objekt hinzugefügt
print(p.last_name) # Printet: Jameson

 Vererbung/Downcasting

Pybind11 kann auch Klassen mit Vererbung binden. Nachfolgend werden 2 Strukturen mit einer Vererbung gezeigt:

struct Animal {
    Animal(const std::string &name) : name(name) {}

    std::string name;
};

struct Cat : Animal {
    Cat(const std::string &name) : Animal(name) {}
    std::string speak() const { return "Miauu"; }
};

Um die Vererbung zu binden benötigt Pybind die folgenden Spezifikationen:

py::class_<Animal>(m, "Animal")
    .def(py::init<const std::string &>())
    .def_readwrite("name", &Animal::name);

py::class_<Cat, Animal>(m, "Cat")
    .def(py::init<const std::string &>())
    .def("speak", &Cat::speak);

Nach dem Export kann das Struct Cat in Python so genutzt werden:

import pybindExample as m


def test_main():
    cat_example = m.Cat("Garfield")
    print(cat_example.name) # Printet: Garfield
    print(cat_example.speak()) # Printet: Miauu


if __name__ == "__main__":
    test_main()

Funktionen überladen

Um Funktionen zu überladen muss man die Funktionen zu Function-Pointers casten. Das folgende Beispiel zeigt ein Struct mit mehrerer überladenen Funktionen:

struct Person { 
    Person(const std::string &name, int age) : name(name), age(age) { } 
    
    void set(const std::string &newName) { 
        name = newName; 
    }

    void set(int newAge) {
        age = newAge;
    }
    
    const std::string &getName() const { 
        return name;
    }

    int getAge() const {
        return age;
    }
    
    std::string name;
    int age;
};

Um das Binding machen zu können, muss dafür folgendes definiert werden:

py::class_<Person>(m, "Person")
    .def(py::init<const std::string &, int>()) 
    .def("set", static_cast<void (Person::*)(const std::string &)>(&Person::set))
    .def("set", static_cast<void (Person::*)(int)>(&Person::set)) 
    .def("getName", &Person::getName)
    .def("getAge", &Person::getAge);

Nun kann man in Python die Set-Funktion aufrufen und je nach Parameter wird dann die entsprechende Funktion aufgerufen:

import pybindExample as m


def test_main():
    pers = m.Person("Martin", 22)
    print(pers.getName()) # Printet: Martin
    print(pers.getAge()) # Printet: 22
    pers.set("Christoph")
    pers.set(45)
    print(pers.getName()) # Printet: Christoph
    print(pers.getAge()) # Printet: 45


if __name__ == "__main__":
    test_main()

Binding Code auf mehrere Module verteilen

Es ist nicht zwingend notwendig, dass man den Binding Code in nur einem File erstellt. Das folgende Beispiel (gleiche Structs wie aus Abschnitt Vererbung/Downcasting) zeigt, wie man dies realisiert. Angenommen, in einem Modul mit dem Namen Animal hat den folgenden Binding Code:

py::class_<Animal> pet(m, "Animal");
    pet.def(py::init<const std::string &>())
   .def_readwrite("name", &Animal::name);

Wenn man nun in einem weiteren Modul den Binding Code für ein weiteres Struct definiert, welches vom Animal Struct erbt, muss dieses zuerst importiert werden:

py::object animal = (py::object) py::module_::import("Animal").attr("Animal");

py::class_<Cat>(m, "Cat", animal)
    .def(py::init<const std::string &>())
    .def("speak", &Cat::speak);

Return Types

Pybind11 bietet sogenannte Return Type Policies an, da bei Python und C++ das Memory-Management und die Lebensdauer von Objekten unterschiedlich ist. Es ist daher wichtig zu wissen, welche Seite der Besitzer des Objektes ist um Memory Leaks oder Datenkorruption zu vermeiden. Das Folgende Beispiel zeigt dies:

// C++ Funktion
Data* getData() { return m_data; } // Beispiel mit Pointer auf statische Datenstruktur

// Binding Code
m.def("get_data", &getData); // Wird zu einem Absturtz führen, wenn diese FUnktion von Python aufgerufen wird

Zur Erklärung: Es gibt 7 verschiedene Return Type Policies. Default mässig wird die Policy py::return_value_policy::automaticverwendet. In diesem Beispiel führt dies aber dazu, dass Python annimmt, dass es der Besitzer der erhaltenen Datenstruktur ist. Somit wird der Garbage Collector zu einem späteren Zeitpunkt die Referenz löschen und es entsteht eine Datenkorruption. Um dies zu verhindern, muss der Binding Code mit einer anderen Return Type Policy  ausgestattet werden. In diesem Fall wäre dies py::return_value_policy::reference:

m.def("get_data", &get_data, py::return_value_policy::reference);

Dies macht Python klar, dass es nicht der Besitzer des erhaltenen Rückgabewertes ist. In der Dokumentation von Pybind11 sind alle Return Type Policies zu finden unter diesem Link: Pybind11 Return Type Policies

Somit haben wir nun die wichtigsten Eigenschaften und Möglichkeiten von Pybind11 behandelt. Die Dokumentation von Pybind11 bietet noch weitere Snippets wie man beispielsweise Enums exportieren kann ect. und ist ein guten Nachschlagewerk um noch mehr Informationen zu erhalten.

Abschliessende Worte

Pybind11 macht es relativ einfach, Binding-Code zu schreiben und bestehende Bibliotheken in C++ für Python zugänglich zu machen. Was eventuell ein kleiner Nachteil sein könnte, ist die Abhängigkeit des C++ Standards, da C++ Versionen kleiner C++11 nicht unterstützt werden. Man sollte ausserdem die Dokumentation von Pybind11 genau durchlesen, da es sonst zu kritischen Problemen kommen kann wie der letzte Abschnitt verdeutlicht.

Kommentare

Eine Antwort zu “C++ und Python mit Pybind11”

  1. Daniel Dörig sagt:

    Sehr schön aufgebaut und verständlich erklärt!
    Könnte mir in Zukunft nützlich sein bei Integrationstests mit Python von c++-Code.

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden

Copyright © 2025 Noser Engineering AG – Alle Rechte vorbehalten.

NACH OBEN
Privacy Policy Cookie Policy
Zur Webcast Übersicht