带 C 类型的 Python,经过编译
Cython 是一种语言的编译器,这种语言就是 Python 加上可选的 C 类型声明。你编写 .pyx 文件,Cython 把它翻译成 C,C 编译器再把它变成扩展模块,然后像其他模块一样 import。用这种方式编译的普通 Python 代码已经略快一些,但真正的提升来自声明类型:cdef double total 告诉 Cython total 是机器数字而不是 Python 对象,于是循环直接在原始数字上运行,没有解释器开销。繁重的数值循环可以变快五十到一百倍,与 Numba 类似,但你得到的是一个可以发布的普通编译模块。
# 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 函数。数组以类型化内存视图传入,例如一维 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 就能加载它。快速实验可以用 pyximport,它在导入时编译。要看哪里仍然慢,运行 cython -a pricing.pyx:它会生成一个 HTML 页面,每一行按使用的 Python 机制多少涂上黄色,白色的行是纯 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 类型映射到 C25 分 - 用
cdef_signature写出函数头25 分 - 用
declare_locals给局部变量加类型25 分 - 用
build_setup写构建脚本25 分