Du Python avec des types C, compilé
Cython est un compilateur pour un langage qui est du Python plus des déclarations de types C optionnelles. Vous écrivez un fichier .pyx, Cython le traduit en C, un compilateur C en fait un module d'extension, et vous l'importez comme n'importe quel module. Du code Python simple compilé ainsi va déjà un peu plus vite, mais le vrai gain vient quand on déclare des types : cdef double total dit à Cython que total est un nombre machine, pas un objet Python, si bien que la boucle travaille sur des nombres bruts sans surcoût d'interpréteur. Les boucles numériques lourdes peuvent devenir cinquante à cent fois plus rapides, comme avec Numba, mais on obtient un module compilé ordinaire que l'on peut distribuer.
# What a Cython file looks like: Python with cdef type declarations (shown here as text)
pyx = """\
cpdef double dot(double[:] a, double[:] b):
cdef double total = 0.0
cdef long i
for i in range(a.shape[0]):
total += a[i] * b[i]
return total
"""
print(pyx.splitlines()[0]) # cpdef double dot(double[:] a, double[:] b):
print(sum("cdef" in line for line in pyx.splitlines())) # 2 two typed declarations inside the functionDéclarer des types : cdef, cpdef et vues mémoire
cdef déclare une variable C (cdef double total, cdef long i) ou une fonction que seul du code C peut appeler. cpdef déclare une fonction appelable depuis Python et depuis C, ce qui convient aux points d'entrée d'un module. def reste une fonction Python ordinaire. Les tableaux se passent en vues mémoire typées, comme double[:] pour un tableau unidimensionnel de doubles : elles acceptent les tableaux NumPy sans copie et s'indexent à la vitesse du C. Une signature comme cpdef double mean(double[:] x, long n) remplace donc les types dynamiques de Python par des types exacts, et un type omis, ou déclaré object, reste un objet Python lent.
# Building the declarations as text: one table from Python type names to C types
C_TYPES = {"int": "long", "float": "double", "bool": "bint", "array[float]": "double[:]"}
def c_type(py_type):
return C_TYPES.get(py_type, "object")
def cdef_signature(name, params, returns):
result = "void" if returns is None else c_type(returns)
arguments = ", ".join(f"{c_type(t)} {p}" for p, t in params)
return f"cpdef {result} {name}({arguments})"
print(cdef_signature("mean", [("x", "array[float]"), ("n", "int")], "float")) # cpdef double mean(double[:] x, long n)
print(cdef_signature("log_event", [("message", "dict")], None)) # cpdef void log_event(object message)Construire le module et trouver quoi typer
Un setup.py avec cythonize(["pricing.pyx"]) demande à setuptools de traduire et compiler le fichier ; python setup.py build_ext --inplace produit une bibliothèque partagée à côté de votre code, et import pricing la charge. Pour des essais rapides, pyximport compile à l'import. Pour voir ce qui reste lent, lancez cython -a pricing.pyx : il écrit une page HTML où chaque ligne est teintée de jaune selon la quantité de machinerie Python qu'elle utilise, et les lignes blanches sont du C pur. Typez les variables des lignes jaunes dans vos boucles critiques, puis désactivez les contrôles dont vous avez prouvé l'inutilité avec @cython.boundscheck(False) et @cython.wraparound(False) pour que l'indexation coûte aussi peu qu'en C.
# The build script is a few lines of text; this generates it for a list of modules
def build_setup(modules):
files = ", ".join(f'"{module}.pyx"' for module in modules)
return "from setuptools import setup\nfrom Cython.Build import cythonize\n\n" + f"setup(ext_modules=cythonize([{files}]))\n"
print(build_setup(["pricing", "risk"]), end="")
# from setuptools import setup
# from Cython.Build import cythonize
#
# setup(ext_modules=cythonize(["pricing.pyx", "risk.pyx"]))# The loop a .pyx file would hold, as plain Python: same result, compiled once types are added
def dot(a, b):
total = 0.0
for i in range(len(a)):
total += a[i] * b[i]
return total
prices = [184.25, 410.5, 99.0]
quantities = [100.0, 50.0, 200.0]
print(dot(prices, quantities)) # 58750.0 the compiled version returns exactly this number
print(dot(prices, quantities) == sum(p * q for p, q in zip(prices, quantities))) # True- Cython compile un fichier
.pyx(Python plus des types C optionnels) en C puis en module d'extension que l'onimportnormalement. cdefdéclare des variables C et des fonctions réservées à C,cpdefune fonction appelable depuis Python et C ; les tableaux sont des vues mémoire typées commedouble[:].- Construisez avec
cythonizedanssetup.py, utilisezcython -apour trouver les lignes jaunes (lentes) des boucles critiques, et vérifiez avec le résultat Python simple.
À suivre : appeler du C++ avec pybind11, exposer du code C++ existant à Python.
Lab : ce qui est noté
- Associer types Python et types C avec
c_type25 pts - Écrire l'en-tête d'une fonction avec
cdef_signature25 pts - Typer les variables locales avec
declare_locals25 pts - Écrire le script de compilation avec
build_setup25 pts