Quand Python a besoin de C++
Une bonne part du code sérieux du monde du trading est du C++ : moteurs d'appariement, gestionnaires de données de marché, bibliothèques de valorisation affinées pendant quinze ans. On veut rarement le réécrire, et rarement renoncer à Python pour la recherche. pybind11 est le pont : une bibliothèque C++ à en-têtes seuls dans laquelle on écrit quelques lignes disant « expose cette fonction C++ à Python sous ce nom », on les compile en bibliothèque partagée, et on l'importe depuis Python comme n'importe quel module. Les arguments et résultats sont convertis pour vous : une liste Python de flottants devient un std::vector<double> et inversement.
# A C++ function and the pybind11 file that exposes it to Python (shown here as text)
cpp = """\
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <vector>
namespace py = pybind11;
double mean(const std::vector<double>& x) {
double total = 0.0;
for (double v : x) total += v;
return x.empty() ? 0.0 : total / x.size();
}
PYBIND11_MODULE(fastmath, m) {
m.def("mean", &mean, "Mean of a list of floats");
}
"""
lines = cpp.splitlines()
print(lines[-2].strip()) # m.def("mean", &mean, "Mean of a list of floats");
print(len(lines)) # 15Les types de part et d'autre du pont
Chaque type Python a un équivalent C++ : int est int64_t, float est double, str est std::string, une liste de flottants est std::vector<double> ; tout le reste peut être tenu comme un py::object générique. Inclure <pybind11/stl.h> rend automatiques les conversions des conteneurs standard. Passez les grosses valeurs par référence constante (const std::vector<double>& x) pour que la fonction C++ ne les copie pas, et les petites valeurs comme les nombres par valeur. La conversion d'une liste Python en vecteur copie quand même une fois par appel : pour de grands tableaux, utilisez py::array_t<double>, qui voit directement la mémoire d'un tableau NumPy sans copie.
# Choosing the C++ type of each argument; big values are passed by const reference
CPP_TYPES = {"int": "int64_t", "float": "double", "str": "std::string", "list[float]": "std::vector<double>"}
def cpp_type(py_type):
return CPP_TYPES.get(py_type, "py::object")
def cpp_signature(name, params, returns):
result = "void" if returns is None else cpp_type(returns)
arguments = []
for param, py_type in params:
ctype = cpp_type(py_type)
arguments.append(f"const {ctype}& {param}" if ctype.startswith("std::") else f"{ctype} {param}")
return f"{result} {name}({', '.join(arguments)})"
print(cpp_signature("mean", [("x", "list[float]"), ("n", "int")], "float")) # double mean(const std::vector<double>& x, int64_t n)
print(cpp_signature("label", [("symbol", "str")], "str")) # std::string label(const std::string& symbol)Compilation, GIL et propriété
On compile avec CMake (pybind11_add_module) ou avec pybind11.setup_helpers dans un setup.py, et le résultat est un fichier .so ou .pyd que l'on importe. Deux choses à savoir. D'abord, pendant que le code C++ s'exécute, le verrou global de l'interpréteur (GIL) reste tenu sauf si on le relâche avec py::gil_scoped_release ; un long calcul C++ qui ne touche pas aux objets Python devrait le relâcher pour que d'autres threads Python tournent. Ensuite, la propriété : si le C++ renvoie une référence ou un pointeur vers sa propre mémoire, Python ne doit pas lui survivre (py::return_value_policy contrôle cela), et un plantage sans trace Python signale en général un pointeur pendant. Testez les fonctions liées depuis Python contre une version Python pure, exactement comme dans les labos précédents.
# Binding lines and the module file, generated from a table of functions
BACKSLASH = chr(92)
def binding_line(py_name, cpp_name, doc):
escaped = doc.replace(BACKSLASH, BACKSLASH + BACKSLASH).replace('"', BACKSLASH + '"')
return f'm.def("{py_name}", &{cpp_name}, "{escaped}");'
def module_source(module, functions):
lines = ["#include <pybind11/pybind11.h>", "#include <pybind11/stl.h>", "", "namespace py = pybind11;", ""]
lines.append(f"PYBIND11_MODULE({module}, m) {{")
for py_name, cpp_name, doc in functions:
lines.append(" " + binding_line(py_name, cpp_name, doc))
lines.append("}")
return "\n".join(lines) + "\n"
source = module_source("pricing", [("mean", "mean_impl", "Mean of x"), ("price", "bs_price", 'Black-Scholes "call" price')])
print(source.splitlines()[-3]) # m.def("mean", &mean_impl, "Mean of x");
print(source.splitlines()[-2]) # m.def("price", &bs_price, "Black-Scholes \"call\" price");# From Python the built module is used like any other; this stand-in has the same interface
import types
def mean(x):
"""Mean of a list of floats"""
return sum(x) / len(x) if x else 0.0
fastmath = types.SimpleNamespace(mean=mean) # stands in for `import fastmath` after the build
print(fastmath.mean([100.0, 102.0, 101.0])) # 101.0
print(fastmath.mean.__doc__) # Mean of a list of floats the doc string given to m.def
print(fastmath.mean([])) # 0.0 what the C++ code returns for an empty vector- pybind11 expose des fonctions C++ à Python avec
PYBIND11_MODULEet des lignesm.def(...); le résultat est un module.so/.pydimportable. - Les types correspondent :
int→int64_t,float→double,str→std::string, listes→std::vector; passez les grosses valeurs par référence constante et les tableaux NumPy enpy::array_t. - Relâchez le GIL pour les longs travaux en C++ pur, surveillez qui possède la mémoire renvoyée, validez les entrées à la frontière et testez contre une version Python pure.
À suivre : les threads et le GIL, ce que les threads Python peuvent et ne peuvent pas faire en parallèle.
Lab : ce qui est noté
- Associer types Python et types C++ avec
cpp_type25 pts - Déclarer une fonction avec
cpp_signature25 pts - Exposer une fonction avec
binding_line25 pts - Assembler le module avec
module_source25 pts