C の型を持つ Python をコンパイルする
Cython は、Python にオプションの C 型宣言を加えた言語のコンパイラです。.pyx ファイルを書くと、Cython がそれを C に翻訳し、C コンパイラが拡張モジュールにして、他のモジュールと同じように import できます。素の Python コードをこうしてコンパイルするだけでも少し速くなりますが、本当の効果は型を宣言したときに出ます。cdef double total は、total が Python オブジェクトではなくマシンの数値だと Cython に伝えるので、ループはインタプリタのオーバーヘッドなしに生の数値で動きます。重い数値ループは Numba と同様に50〜100倍速くなりえますが、配布できる普通のコンパイル済みモジュールが得られます。
# 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 function型の宣言:cdef、cpdef、メモリビュー
cdef は C の変数(cdef double total、cdef long i)、または C のコードからしか呼べない関数を宣言します。cpdef は Python からも C からも呼べる関数を宣言し、モジュールの入口に向いています。def は通常の Python 関数のままです。配列は型付きメモリビューで渡します。1次元の double 配列なら double[:] で、NumPy 配列をコピーなしで受け取り、C の速度でインデックスできます。cpdef double mean(double[:] x, long n) のようなシグネチャは、Python の動的な型を正確な型に置き換えます。型を省略したり object と宣言したりすると、遅い Python オブジェクトのままです。
# 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)モジュールのビルドと、型を付ける場所の見つけ方
cythonize(["pricing.pyx"]) を呼ぶ setup.py は、setuptools にファイルの翻訳とコンパイルを指示します。python setup.py build_ext --inplace でコードの隣に共有ライブラリができ、import pricing で読み込まれます。手軽な試用には、import 時にコンパイルする pyximport があります。まだ遅い箇所を知るには cython -a pricing.pyx を実行します。各行が使っている Python の仕組みの量に応じて黄色で塗られた HTML ページが作られ、白い行は純粋な C です。ホットなループ内の黄色い行の変数に型を付け、不要だと確認したチェックを @cython.boundscheck(False) と @cython.wraparound(False) で無効にすると、インデックス参照が 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 は
.pyxファイル(Python に任意の C 型を加えたもの)を C、さらに拡張モジュールにコンパイルし、普通にimportできる cdefは C の変数と C 専用の関数を、cpdefは Python と C の両方から呼べる関数を宣言する。配列はdouble[:]のような型付きメモリビューsetup.pyのcythonizeでビルドし、cython -aでホットループ内の黄色(遅い)行を探し、素の Python の結果と照合する
次は pybind11 による C++ の呼び出しです。既存の C++ コードを Python に公開します。
ラボ:採点される内容
c_typeで Python の型を C の型に対応づける25点cdef_signatureで関数ヘッダーを書く25点declare_localsでローカル変数に型を付ける25点build_setupでビルドスクリプトを書く25点