Wenn Python C++ braucht
Ein großer Teil des ernsthaften Codes der Tradingwelt ist C++: Matching-Engines, Marktdaten-Handler, über fünfzehn Jahre abgestimmte Bewertungsbibliotheken. Man will ihn selten neu schreiben und selten Python für die Recherche aufgeben. pybind11 ist die Brücke: eine Header-only-C++-Bibliothek, in der Sie wenige Zeilen schreiben, die sagen „mache diese C++-Funktion unter diesem Namen für Python verfügbar“, sie zu einer Shared Library kompilieren und aus Python wie jedes Modul importieren. Argumente und Ergebnisse werden für Sie konvertiert: Eine Python-Liste von Floats wird zu einem std::vector<double> und zurück.
# 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)) # 15Typen über die Brücke
Jeder Python-Typ hat ein C++-Gegenstück: int ist int64_t, float ist double, str ist std::string, eine Liste von Floats ist std::vector<double>; alles andere lässt sich als generisches py::object halten. Das Einbinden von <pybind11/stl.h> macht die Konvertierung von Standardcontainern automatisch. Übergeben Sie große Werte als const-Referenz (const std::vector<double>& x), damit die C++-Funktion sie nicht kopiert, und kleine Werte wie Zahlen als Wert. Die Umwandlung einer Python-Liste in einen Vektor kopiert dennoch einmal pro Aufruf, daher nutzen Sie für große Arrays py::array_t<double>, das den Speicher eines NumPy-Arrays direkt ohne Kopie sieht.
# 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)Build, GIL und Besitz
Gebaut wird mit CMake (pybind11_add_module) oder mit pybind11.setup_helpers in einer setup.py, und das Ergebnis ist eine .so- oder .pyd-Datei, die man importiert. Zwei Dinge sind wichtig. Erstens bleibt die Global Interpreter Lock (GIL) während der Ausführung von C++-Code gehalten, außer Sie geben sie mit py::gil_scoped_release frei; eine lange C++-Berechnung, die keine Python-Objekte anfasst, sollte sie freigeben, damit andere Python-Threads laufen können. Zweitens der Besitz: Gibt C++ eine Referenz oder einen Zeiger auf seinen eigenen Speicher zurück, darf Python ihn nicht überleben (py::return_value_policy steuert das), und ein Absturz ohne Python-Traceback deutet meist auf einen hängenden Zeiger. Testen Sie die gebundenen Funktionen aus Python gegen eine reine Python-Version, genau wie in den vorigen Labs.
# 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 stellt C++-Funktionen mit
PYBIND11_MODULEundm.def(...)-Zeilen für Python bereit; das Ergebnis ist ein importierbares.so/.pyd-Modul. - Typen entsprechen sich:
int→int64_t,float→double,str→std::string, Listen→std::vector; große Werte als const-Referenz, NumPy-Arrays alspy::array_tübergeben. - Geben Sie die GIL bei langer reiner C++-Arbeit frei, achten Sie darauf, wem zurückgegebener Speicher gehört, validieren Sie Eingaben an der Grenze und testen Sie gegen eine reine Python-Version.
Als Nächstes: Threads und die GIL, was Python-Threads parallel können und was nicht.
Lab: Was bewertet wird
- Python-Typen mit
cpp_typeauf C++ abbilden25 Pkt. - Mit
cpp_signatureeine Funktion deklarieren25 Pkt. - Mit
binding_lineeine Funktion freigeben25 Pkt. - Mit
module_sourcedas Modul zusammensetzen25 Pkt.