Skip to content

Latest commit

 

History

History
934 lines (723 loc) · 27 KB

File metadata and controls

934 lines (723 loc) · 27 KB

🔝 Retour au Sommaire

43.2.2 — Exposition de fonctions et classes

Module 15 : Interopérabilité · Niveau Expert


Introduction

La section précédente a mis en place l'environnement : pybind11 est installé, CMake est configuré, le module compile et se charge depuis Python. Cette section entre dans le cœur du sujet : la syntaxe des bindings — comment déclarer à pybind11 quelles fonctions, classes, méthodes, propriétés et types C++ doivent être accessibles depuis Python, et sous quelle forme.

pybind11 adopte une approche déclarative. On ne génère pas de code intermédiaire (contrairement à SWIG), on n'écrit pas de fichier de description externe (contrairement à Cython). On écrit directement du C++ dans le bloc PYBIND11_MODULE, en chaînant des appels .def(...) qui décrivent chaque élément exposé. Le compilateur C++ déduit les types à partir des pointeurs de fonctions et de méthodes, et pybind11 génère automatiquement le code de conversion.

Tout au long de cette section, on utilisera un en-tête commun dans les fichiers de binding :

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>           // Conversions std::vector, std::map, etc.
#include <pybind11/operators.h>     // Surcharge d'opérateurs (si nécessaire)

namespace py = pybind11;  
using namespace pybind11::literals; // Permet la syntaxe "name"_a pour les arguments  

43.2.2.1 — Fonctions libres

Exposition basique

La forme la plus simple d'un binding est l'exposition d'une fonction libre C++ :

// C++ — code métier (dans un .h/.cpp séparé)
double celsius_to_fahrenheit(double celsius) {
    return celsius * 9.0 / 5.0 + 32.0;
}
// Binding
PYBIND11_MODULE(tempconv, m) {
    m.doc() = "Temperature conversion utilities";

    m.def("celsius_to_fahrenheit", &celsius_to_fahrenheit,
          "Convert a temperature from Celsius to Fahrenheit");
}
# Python
import tempconv  
print(tempconv.celsius_to_fahrenheit(100.0))  # 212.0  

Le premier argument de m.def() est le nom visible en Python (il peut différer du nom C++). Le second est un pointeur vers la fonction. Le troisième (optionnel) est la docstring.

Arguments nommés avec py::arg

Par défaut, les arguments d'une fonction exposée n'ont pas de nom côté Python — l'autocomplétion affiche arg0, arg1, etc. Pour fournir des noms significatifs :

m.def("celsius_to_fahrenheit", &celsius_to_fahrenheit,
      py::arg("celsius"),
      "Convert a temperature from Celsius to Fahrenheit");

Avec le using namespace pybind11::literals, on peut utiliser la syntaxe abrégée "name"_a :

m.def("celsius_to_fahrenheit", &celsius_to_fahrenheit,
      "celsius"_a,
      "Convert a temperature from Celsius to Fahrenheit");

En Python, les deux formes permettent l'appel nommé :

tempconv.celsius_to_fahrenheit(celsius=37.0)  # 98.6

Valeurs par défaut

Les valeurs par défaut des arguments C++ ne sont pas accessibles via un pointeur de fonction — pybind11 ne peut pas les déduire automatiquement. Il faut les redéclarer dans le binding :

// C++
std::string format_value(double value, int precision = 2, bool scientific = false);
// Binding — les valeurs par défaut doivent être répétées
m.def("format_value", &format_value,
      "value"_a,
      "precision"_a = 2,
      "scientific"_a = false,
      "Format a numeric value as a string");
# Python — les trois formes sont valides
format_value(3.14159)  
format_value(3.14159, precision=4)  
format_value(3.14159, scientific=True)  

Surcharge de fonctions (overloads)

Le C++ permet la surcharge de fonctions — plusieurs fonctions portant le même nom avec des signatures différentes. Un pointeur brut &compute est ambigu si plusieurs surcharges existent. pybind11 fournit le cast explicite pour lever l'ambiguïté :

// C++ — deux surcharges
double compute(double x);  
double compute(double x, double y);  
// Binding — cast explicite vers la bonne signature
m.def("compute", static_cast<double(*)(double)>(&compute),
      "x"_a);
m.def("compute", static_cast<double(*)(double, double)>(&compute),
      "x"_a, "y"_a);

Python gère naturellement la dispatch par nombre d'arguments :

compute(3.0)       # Appelle compute(double)  
compute(3.0, 4.0)  # Appelle compute(double, double)  

pybind11 essaie chaque surcharge dans l'ordre d'enregistrement et choisit la première qui correspond aux types des arguments passés. En cas d'ambiguïté (par exemple int vs double pour un argument Python int), l'ordre d'enregistrement détermine la priorité.

💡 C++14 et ultérieur : py::overload_cast offre une syntaxe plus lisible que static_cast :

m.def("compute", py::overload_cast<double>(&compute), "x"_a);  
m.def("compute", py::overload_cast<double, double>(&compute), "x"_a, "y"_a);  

Fonctions lambda dans le binding

On peut exposer directement une lambda C++ comme fonction Python. C'est utile pour adapter une interface C++ qui ne correspond pas exactement à ce qu'on veut exposer en Python :

m.def("version", []() {
    return std::string("1.0.0");
});

// Adapter une API existante
m.def("quick_compute", [](double x) {
    // Appelle une API C++ interne complexe avec des paramètres par défaut raisonnables
    Config cfg = Config::default_config();
    return internal_compute(x, cfg);
}, "x"_a, "Simplified compute with default configuration");

Les lambdas sont aussi le mécanisme principal pour exposer des fonctions dont la signature C++ n'est pas directement traduisible en Python (paramètres de sortie par pointeur, valeurs de retour multiples, etc.) :

// C++ — Signature typique C avec paramètres de sortie
bool parse_coordinates(const char* input, double* lat, double* lon);
// Binding — La lambda adapte l'interface
m.def("parse_coordinates", [](const std::string& input) {
    double lat, lon;
    bool ok = parse_coordinates(input.c_str(), &lat, &lon);
    if (!ok) throw std::runtime_error("Invalid coordinate string");
    return std::make_tuple(lat, lon);   // Retourne un tuple Python
}, "input"_a, "Parse a coordinate string, returns (lat, lon)");
lat, lon = parse_coordinates("48.8566,2.3522")

43.2.2.2 — Classes

Exposition basique d'une classe

// C++ — code métier
class Sensor {  
public:  
    explicit Sensor(std::string id) : id_(std::move(id)), value_(0.0) {}

    void record(double measurement) { value_ = measurement; }
    double value() const { return value_; }
    const std::string& id() const { return id_; }

private:
    std::string id_;
    double      value_;
};
// Binding
PYBIND11_MODULE(sensors, m) {
    py::class_<Sensor>(m, "Sensor")
        .def(py::init<std::string>(), "id"_a)
        .def("record", &Sensor::record, "measurement"_a)
        .def("value", &Sensor::value)
        .def("id", &Sensor::id);
}
s = Sensor("temp_01")  
s.record(22.5)  
print(s.value())  # 22.5  
print(s.id())     # "temp_01"  

Anatomie de py::class_<Sensor>(m, "Sensor") :

  • Le paramètre template Sensor est le type C++ à exposer.
  • m est le module dans lequel la classe est enregistrée.
  • "Sensor" est le nom visible en Python (peut différer du nom C++).
  • L'objet retourné supporte le chaînage de .def(...) pour déclarer constructeurs, méthodes et propriétés.

Constructeurs

py::init<...>() déclare un constructeur. Les paramètres template correspondent aux types des arguments du constructeur C++ :

py::class_<Sensor>(m, "Sensor")
    // Constructeur avec un argument std::string
    .def(py::init<std::string>(), "id"_a)

    // Plusieurs constructeurs (surcharge)
    .def(py::init<std::string, double>(), "id"_a, "initial_value"_a);

Pour des constructeurs complexes (factory, paramètres non triviaux), on utilise py::init(lambda) :

py::class_<Engine>(m, "Engine")
    .def(py::init([](const std::string& config_path) {
        auto cfg = Config::load_from_file(config_path);
        return std::make_unique<Engine>(std::move(cfg));
    }), "config_path"_a, "Create an engine from a configuration file");

Méthodes

Les méthodes sont exposées avec .def("nom_python", &Classe::methode). Le premier argument est le nom visible en Python, le second est un pointeur de méthode :

py::class_<Sensor>(m, "Sensor")
    .def(py::init<std::string>(), "id"_a)

    // Méthode non-const
    .def("record", &Sensor::record, "measurement"_a)

    // Méthode const
    .def("value", &Sensor::value)

    // Nom Python différent du nom C++
    .def("get_identifier", &Sensor::id, "Return the sensor identifier");

Surcharge de méthodes

Comme pour les fonctions libres, la surcharge de méthodes nécessite un cast explicite ou py::overload_cast :

class Logger {  
public:  
    void log(const std::string& message);
    void log(const std::string& message, int severity);
};
py::class_<Logger>(m, "Logger")
    .def(py::init<>())
    .def("log", py::overload_cast<const std::string&>(&Logger::log),
         "message"_a)
    .def("log", py::overload_cast<const std::string&, int>(&Logger::log),
         "message"_a, "severity"_a);

Pour lever l'ambiguïté entre une surcharge const et une surcharge non-const de la même signature, ajouter py::const_ :

class Container {  
public:  
    int&       at(size_t index);        // non-const
    const int& at(size_t index) const;  // const
};
// Exposer la version const
.def("at", py::overload_cast<size_t>(&Container::at, py::const_), "index"_a)

43.2.2.3 — Propriétés

Propriétés en lecture seule

Plutôt que d'exposer un getter comme méthode (sensor.id()), on peut l'exposer comme propriété Python (sensor.id) — plus idiomatique :

py::class_<Sensor>(m, "Sensor")
    .def(py::init<std::string>(), "id"_a)
    .def_property_readonly("id", &Sensor::id)
    .def_property_readonly("value", &Sensor::value);
s = Sensor("temp_01")  
s.record(22.5)  
print(s.id)       # "temp_01"  — accès comme attribut, pas comme méthode  
print(s.value)    # 22.5  
s.id = "other"    # AttributeError: can't set attribute — lecture seule  

Propriétés en lecture-écriture

.def_property prend un getter et un setter :

class Config {  
public:  
    int timeout() const { return timeout_; }
    void set_timeout(int ms) { timeout_ = ms; }
private:
    int timeout_ = 5000;
};
py::class_<Config>(m, "Config")
    .def(py::init<>())
    .def_property("timeout", &Config::timeout, &Config::set_timeout);
cfg = Config()  
print(cfg.timeout)   # 5000  
cfg.timeout = 3000   # Appelle set_timeout(3000)  
print(cfg.timeout)   # 3000  

Accès direct aux membres publics

Pour les structures simples avec des membres publics, def_readwrite et def_readonly évitent d'écrire des getters/setters :

struct Point {
    double x = 0.0;
    double y = 0.0;
    double z = 0.0;
};
py::class_<Point>(m, "Point")
    .def(py::init<>())
    .def_readwrite("x", &Point::x)
    .def_readwrite("y", &Point::y)
    .def_readwrite("z", &Point::z);
p = Point()  
p.x = 1.0  
p.y = 2.0  
print(p.x, p.y, p.z)  # 1.0 2.0 0.0  

Pour un membre const ou qu'on veut protéger en écriture :

.def_readonly("z", &Point::z)

43.2.2.4 — Méthodes et attributs statiques

class Registry {  
public:  
    static Registry& instance();
    static int count();

    void register_item(const std::string& name);
    static constexpr int MAX_ITEMS = 1024;

private:
    Registry() = default;
};
py::class_<Registry>(m, "Registry")
    // Méthode statique
    .def_static("instance", &Registry::instance, py::return_value_policy::reference)
    .def_static("count", &Registry::count)

    // Méthode d'instance
    .def("register_item", &Registry::register_item, "name"_a)

    // Attribut statique en lecture seule
    .def_readonly_static("MAX_ITEMS", &Registry::MAX_ITEMS);
reg = Registry.instance()  
print(Registry.count())       # 0  
reg.register_item("sensor_a")  
print(Registry.count())       # 1  
print(Registry.MAX_ITEMS)     # 1024  

💡 Le py::return_value_policy::reference pour instance() indique à pybind11 de ne pas prendre la propriété de l'objet retourné — le singleton est géré côté C++. Les return value policies sont détaillées en section 43.2.3.


43.2.2.5 — Enums

Enum classique (scoped enum)

enum class LogLevel {
    Debug   = 0,
    Info    = 1,
    Warning = 2,
    Error   = 3,
    Fatal   = 4
};
py::enum_<LogLevel>(m, "LogLevel")
    .value("Debug",   LogLevel::Debug)
    .value("Info",    LogLevel::Info)
    .value("Warning", LogLevel::Warning)
    .value("Error",   LogLevel::Error)
    .value("Fatal",   LogLevel::Fatal)
    .export_values();  // Optionnel : rend les valeurs accessibles sans préfixe
from sensors import LogLevel

level = LogLevel.Warning  
print(level)            # LogLevel.Warning  
print(int(level))       # 2  
print(level == LogLevel.Warning)  # True  

L'appel .export_values() rend les valeurs accessibles directement dans le module (sensors.Warning en plus de sensors.LogLevel.Warning). Pour les enums scopées C++, il est généralement préférable de ne pas appeler .export_values() afin de préserver le scoping.

Enum rattachée à une classe

On peut déclarer l'enum à l'intérieur du scope d'une classe Python :

py::class_<Logger> logger_class(m, "Logger");

py::enum_<LogLevel>(logger_class, "Level")
    .value("Debug",   LogLevel::Debug)
    .value("Info",    LogLevel::Info)
    .value("Warning", LogLevel::Warning)
    .value("Error",   LogLevel::Error)
    .value("Fatal",   LogLevel::Fatal);

logger_class
    .def(py::init<>())
    .def("set_level", &Logger::set_level, "level"_a);
logger = Logger()  
logger.set_level(Logger.Level.Warning)  

43.2.2.6 — Héritage et polymorphisme

Héritage simple

Pour exposer une hiérarchie de classes, on indique la classe parente dans les paramètres template de py::class_ :

// C++
class Shape {  
public:  
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

class Circle : public Shape {  
public:  
    explicit Circle(double radius) : radius_(radius) {}
    double area() const override { return 3.14159265 * radius_ * radius_; }
    std::string name() const override { return "Circle"; }
    double radius() const { return radius_; }
private:
    double radius_;
};

class Rectangle : public Shape {  
public:  
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double area() const override { return width_ * height_; }
    std::string name() const override { return "Rectangle"; }
private:
    double width_, height_;
};
// Binding
py::class_<Shape>(m, "Shape")
    .def("area", &Shape::area)
    .def("name", &Shape::name);

// Le second paramètre template indique la classe parente
py::class_<Circle, Shape>(m, "Circle")
    .def(py::init<double>(), "radius"_a)
    .def_property_readonly("radius", &Circle::radius);

py::class_<Rectangle, Shape>(m, "Rectangle")
    .def(py::init<double, double>(), "width"_a, "height"_a);

Grâce à la déclaration d'héritage, le polymorphisme fonctionne naturellement en Python :

def print_shape_info(shape):
    """Accepte n'importe quel Shape"""
    print(f"{shape.name()}: area = {shape.area():.2f}")

c = Circle(5.0)  
r = Rectangle(3.0, 4.0)  

print_shape_info(c)  # Circle: area = 78.54  
print_shape_info(r)  # Rectangle: area = 12.00  

Une fonction C++ qui accepte un Shape* ou un const Shape& acceptera un Circle ou un Rectangle depuis Python — la vtable C++ est correctement résolue.

Permettre l'héritage depuis Python (trampoline)

Par défaut, les classes exposées via pybind11 ne peuvent pas être sous-classées en Python — les appels virtuels ne seraient pas redirigés vers les méthodes Python. Pour activer cette possibilité, il faut définir une classe trampoline :

// Trampoline : redirige les appels virtuels vers Python
class PyShape : public Shape {  
public:  
    // Hérite des constructeurs
    using Shape::Shape;

    // Override de chaque méthode virtuelle
    double area() const override {
        PYBIND11_OVERRIDE_PURE(
            double,     // Type de retour
            Shape,      // Classe parente
            area        // Nom de la méthode (sans parenthèses)
        );
    }

    std::string name() const override {
        PYBIND11_OVERRIDE_PURE(
            std::string,
            Shape,
            name
        );
    }
};
// Binding avec la classe trampoline
py::class_<Shape, PyShape>(m, "Shape")
    .def(py::init<>())
    .def("area", &Shape::area)
    .def("name", &Shape::name);

Désormais, on peut sous-classer Shape en Python :

class Triangle(Shape):
    def __init__(self, base, height):
        super().__init__()
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

    def name(self):
        return "Triangle"

t = Triangle(6.0, 4.0)  
print_shape_info(t)  # Triangle: area = 12.00  

L'appel t.area() depuis C++ (par exemple dans une fonction qui prend un const Shape&) passe par le trampoline, qui redirige vers la méthode Python Triangle.area.

Les macros disponibles :

Macro Usage
PYBIND11_OVERRIDE_PURE Méthode virtuelle pure (= 0) — le sous-classement Python est obligatoire
PYBIND11_OVERRIDE Méthode virtuelle non-pure — retombe sur l'implémentation C++ si non overridée en Python

Héritage multiple

pybind11 supporte l'héritage multiple en listant toutes les classes parentes dans les paramètres template :

class Serializable {  
public:  
    virtual ~Serializable() = default;
    virtual std::string serialize() const = 0;
};

class SerializableCircle : public Circle, public Serializable {  
public:  
    using Circle::Circle;
    std::string serialize() const override {
        return "{\"type\":\"circle\",\"radius\":" + std::to_string(radius()) + "}";
    }
};
py::class_<Serializable>(m, "Serializable")
    .def("serialize", &Serializable::serialize);

py::class_<SerializableCircle, Circle, Serializable>(m, "SerializableCircle")
    .def(py::init<double>(), "radius"_a);

43.2.2.7 — Méthodes spéciales Python (dunder methods)

pybind11 permet d'implémenter les méthodes spéciales Python (__repr__, __str__, __len__, __getitem__, etc.) pour que les objets C++ se comportent de manière idiomatique en Python.

repr et str

py::class_<Sensor>(m, "Sensor")
    .def(py::init<std::string>(), "id"_a)
    // __repr__ : représentation non ambiguë (pour le debug)
    .def("__repr__", [](const Sensor& s) {
        return "<Sensor id='" + s.id() + "' value=" +
               std::to_string(s.value()) + ">";
    })
    // __str__ : représentation lisible (pour l'affichage)
    .def("__str__", [](const Sensor& s) {
        return "Sensor(" + s.id() + ")";
    });
s = Sensor("temp_01")  
repr(s)   # "<Sensor id='temp_01' value=0.000000>"  
str(s)    # "Sensor(temp_01)"  
print(s)  # "Sensor(temp_01)"  — print() utilise __str__  

Protocole de séquence (len, getitem, iter)

class DataSeries {  
public:  
    void append(double v) { data_.push_back(v); }
    size_t size() const { return data_.size(); }
    double at(size_t i) const {
        if (i >= data_.size()) throw std::out_of_range("Index out of range");
        return data_[i];
    }
    const std::vector<double>& data() const { return data_; }
private:
    std::vector<double> data_;
};
py::class_<DataSeries>(m, "DataSeries")
    .def(py::init<>())
    .def("append", &DataSeries::append, "value"_a)

    // Protocole de séquence
    .def("__len__", &DataSeries::size)
    .def("__getitem__", [](const DataSeries& ds, int64_t index) {
        // Support des index négatifs (convention Python)
        if (index < 0) index += static_cast<int64_t>(ds.size());
        if (index < 0 || static_cast<size_t>(index) >= ds.size())
            throw py::index_error("Index " + std::to_string(index) + " out of range");
        return ds.at(static_cast<size_t>(index));
    })
    .def("__iter__", [](const DataSeries& ds) {
        return py::make_iterator(ds.data().begin(), ds.data().end());
    }, py::keep_alive<0, 1>())  // Garde l'objet vivant pendant l'itération

    // Bonus : __contains__
    .def("__contains__", [](const DataSeries& ds, double value) {
        const auto& d = ds.data();
        return std::find(d.begin(), d.end(), value) != d.end();
    });
ds = DataSeries()  
ds.append(1.0)  
ds.append(2.0)  
ds.append(3.0)  

len(ds)        # 3  
ds[0]          # 1.0  
ds[-1]         # 3.0 — index négatif  
2.0 in ds      # True

for val in ds:
    print(val) # 1.0  2.0  3.0

Le py::keep_alive<0, 1>() dans __iter__ est une policy de durée de vie : elle garantit que l'objet DataSeries (argument 1) reste vivant tant que l'itérateur (retour, index 0) existe. Sans cette policy, le garbage collector Python pourrait détruire l'objet pendant l'itération.

Surcharge d'opérateurs

pybind11 fournit des helpers dans <pybind11/operators.h> pour exposer les opérateurs C++ de manière concise :

#include <pybind11/operators.h>

class Vec2 {  
public:  
    double x, y;
    Vec2(double x, double y) : x(x), y(y) {}
    Vec2 operator+(const Vec2& other) const { return {x + other.x, y + other.y}; }
    Vec2 operator*(double scalar) const { return {x * scalar, y * scalar}; }
    bool operator==(const Vec2& other) const { return x == other.x && y == other.y; }
};
py::class_<Vec2>(m, "Vec2")
    .def(py::init<double, double>(), "x"_a, "y"_a)
    .def_readwrite("x", &Vec2::x)
    .def_readwrite("y", &Vec2::y)

    // Opérateurs via py::self
    .def(py::self + py::self)       // __add__
    .def(py::self * double())       // __mul__
    .def(py::self == py::self)      // __eq__

    .def("__repr__", [](const Vec2& v) {
        return "Vec2(" + std::to_string(v.x) + ", " + std::to_string(v.y) + ")";
    });
a = Vec2(1.0, 2.0)  
b = Vec2(3.0, 4.0)  
c = a + b          # Vec2(4.0, 6.0)  
d = a * 2.0        # Vec2(2.0, 4.0)  
a == Vec2(1.0, 2.0) # True  

43.2.2.8 — Exceptions personnalisées

Au-delà des conversions automatiques (section 43.2), pybind11 permet d'enregistrer des exceptions C++ personnalisées comme exceptions Python :

// C++ — Exception custom
class EngineError : public std::runtime_error {  
public:  
    using std::runtime_error::runtime_error;
    int error_code() const { return code_; }
    void set_code(int c) { code_ = c; }
private:
    int code_ = 0;
};
// Binding — Enregistrement comme exception Python
// Le troisième argument est la classe parente Python (PyExc_RuntimeError)
static py::exception<EngineError> exc_engine(m, "EngineError", PyExc_RuntimeError);

// Si l'exception C++ a des attributs supplémentaires, on utilise un handler
py::register_exception_translator([](std::exception_ptr p) {
    try {
        if (p) std::rethrow_exception(p);
    } catch (const EngineError& e) {
        PyErr_SetString(exc_engine.ptr(), e.what());
    }
});
try:
    engine.execute("invalid_command")
except EngineError as e:
    print(f"Engine failed: {e}")       # Fonctionne comme une RuntimeError
    isinstance(e, RuntimeError)         # True — héritage préservé

43.2.2.9 — Sous-modules

Pour les bibliothèques volumineuses, pybind11 permet d'organiser les bindings en sous-modules Python :

PYBIND11_MODULE(mylib, m) {
    m.doc() = "Main library module";

    // Sous-module "io"
    auto io = m.def_submodule("io", "Input/Output utilities");
    py::class_<FileReader>(io, "FileReader")
        .def(py::init<std::string>(), "path"_a)
        .def("read_all", &FileReader::read_all);

    // Sous-module "math"
    auto math = m.def_submodule("math", "Mathematical operations");
    py::class_<Vec2>(math, "Vec2")
        .def(py::init<double, double>(), "x"_a, "y"_a);

    // Sous-module "math.linalg" (imbrication)
    auto linalg = math.def_submodule("linalg", "Linear algebra");
    linalg.def("dot", &dot_product, "a"_a, "b"_a);
}
from mylib.io import FileReader  
from mylib.math import Vec2  
from mylib.math.linalg import dot  

43.2.2.10 — Organisation recommandée des bindings

Séparer binding et code métier

Le code de binding ne doit jamais être mélangé avec le code métier C++. La bibliothèque C++ doit compiler et fonctionner sans aucune dépendance à pybind11 :

src/
  mylib/
    processor.h        # Pur C++ — aucune mention de pybind11
    processor.cpp      # Pur C++ — aucune mention de pybind11
python/
    bindings.cpp       # Dépend de pybind11 et de mylib

Diviser les bindings par domaine fonctionnel

Pour les projets volumineux, chaque domaine a son propre fichier de binding (voir aussi la section 43.2.1 sur la compilation parallèle) :

// python/bind_core.cpp
#include <pybind11/pybind11.h>
#include "mylib/core.h"

void bind_core(py::module_& m) {
    py::class_<Engine>(m, "Engine")
        .def(py::init<>())
        .def("run", &Engine::run);
}
// python/bind_io.cpp
#include <pybind11/pybind11.h>
#include "mylib/io.h"

void bind_io(py::module_& m) {
    auto io = m.def_submodule("io");
    py::class_<FileReader>(io, "FileReader")
        .def(py::init<std::string>(), "path"_a);
}
// python/bindings.cpp — Point d'entrée unique
#include <pybind11/pybind11.h>

void bind_core(py::module_& m);  
void bind_io(py::module_& m);  

PYBIND11_MODULE(mylib, m) {
    bind_core(m);
    bind_io(m);
}

Ce pattern garantit la compilation parallèle, la compilation incrémentale, et la lisibilité dans les projets à plusieurs dizaines de classes.


Résumé des formes de .def

Forme Usage
m.def("name", &func) Fonction libre
.def(py::init<Args...>()) Constructeur
.def("name", &Class::method) Méthode d'instance
.def_static("name", &Class::static_method) Méthode statique
.def_property_readonly("name", &Class::getter) Propriété lecture seule
.def_property("name", &Class::getter, &Class::setter) Propriété lecture-écriture
.def_readwrite("name", &Class::member) Accès direct membre public
.def_readonly("name", &Class::member) Accès direct lecture seule
.def_readonly_static("name", &Class::static_member) Attribut statique lecture seule
.def("__repr__", lambda) Méthode spéciale Python
.def(py::self + py::self) Surcharge d'opérateur

📎 La section suivante (43.2.3) approfondit la gestion des types : passage par référence vs copie, buffers NumPy, return value policies, et les subtilités de la gestion mémoire à la frontière C++/Python.

⏭️ Gestion des types et conversions