本章介紹 Cython,一種 Python 的超集語言,可以編譯成 C 程式碼。

本章目標

學完本章後,你將能夠:

  1. 理解 Cython 的編譯流程
  2. 使用型別宣告加速程式碼
  3. 使用 Cython 包裝 C 函式庫

【原理層】Cython 是什麼?

Python 的超集

Cython 是一種程式語言,它是 Python 的超集:

1合法的 Python 程式碼 → 合法的 Cython 程式碼
2                    → 但 Cython 可以加入更多語法
3
4Cython 特有語法:
5- cdef:宣告 C 變數或函式
6- cpdef:同時暴露給 Python 和 C
7- cimport:匯入 .pxd 檔案
8- nogil:標記不需要 GIL 的區塊

編譯流程

 1.pyx (Cython 原始碼)
 2 3    ↓ Cython 編譯器
 4.c (C 原始碼)
 5 6    ↓ C 編譯器 (gcc, clang, MSVC)
 7.so / .pyd (Python 擴展模組)
 8 9    ↓ import
10Python 程式

為什麼 Cython 比 Python 快?

 1# 純 Python:每次操作都是物件操作
 2def python_sum(n):
 3    total = 0           # 建立 int 物件
 4    for i in range(n):  # 建立 range 物件,迭代器
 5        total += i      # 呼叫 __add__,建立新物件
 6    return total
 7
 8# Cython:可以使用原生 C 型別
 9def cython_sum(int n):
10    cdef int total = 0  # C 的 int,不是 Python 物件
11    cdef int i
12    for i in range(n):  # 編譯成 C 的 for 迴圈
13        total += i      # 單一 CPU 指令
14    return total

效能差異的來源:

操作PythonCython (有型別)
變數存取dict 查找直接記憶體存取
整數加法物件方法呼叫CPU 指令
迴圈迭代器協議C for 迴圈
函式呼叫建立 frame 物件C 函式呼叫

【設計層】Cython 基礎語法

安裝與設定

1pip install cython
2
3# 檢查版本
4python -c "import cython; print(cython.__version__)"

第一個 Cython 模組

建立 example.pyx

 1# example.pyx
 2
 3def say_hello(name):
 4    """純 Python 函式,也是合法的 Cython"""
 5    print(f"Hello, {name}!")
 6
 7def compute_sum(int n):
 8    """加入型別宣告的函式"""
 9    cdef int i
10    cdef long total = 0
11
12    for i in range(n):
13        total += i
14
15    return total

建立 setup.py

1# setup.py
2from setuptools import setup
3from Cython.Build import cythonize
4
5setup(
6    ext_modules=cythonize("example.pyx"),
7)

編譯與使用:

1# 編譯
2python setup.py build_ext --inplace
3
4# 使用
5python -c "import example; example.say_hello('Cython')"

變數宣告

 1# 型別宣告語法
 2
 3# cdef:宣告 C 變數(只在 Cython 內部可見)
 4cdef int x = 10
 5cdef double y = 3.14
 6cdef char* s = "hello"
 7
 8# 多個變數
 9cdef:
10    int a, b, c
11    double d = 0.0
12    list my_list = []
13
14# 型別推斷(Python 3 風格)
15cdef int x = 10      # 明確宣告
16x: cython.int = 10   # 註解風格(Pure Python 模式)

函式類型

 1# def:Python 函式,可從 Python 呼叫
 2def python_func(x, y):
 3    return x + y
 4
 5# cdef:C 函式,只能從 Cython 呼叫,最快
 6cdef int c_func(int x, int y):
 7    return x + y
 8
 9# cpdef:同時產生 Python 和 C 版本
10cpdef int hybrid_func(int x, int y):
11    return x + y
12
13# 使用情境
14def api_func(int n):
15    """公開 API"""
16    cdef int i
17    cdef int total = 0
18    for i in range(n):
19        total = _helper(total, i)  # 呼叫 cdef 函式
20    return total
21
22cdef int _helper(int a, int b):
23    """內部輔助函式,不暴露給 Python"""
24    return a + b

型別轉換

 1# 隱式轉換
 2cdef int i = 10
 3cdef double d = i  # int → double,自動轉換
 4
 5# 明確轉換
 6cdef double x = 3.14
 7cdef int y = <int>x  # 截斷為 3
 8
 9# Python 物件與 C 型別
10def convert_example(obj):
11    cdef int c_int
12
13    # Python int → C int
14    c_int = <int>obj  # 可能 overflow
15
16    # 安全轉換
17    if isinstance(obj, int) and -2147483648 <= obj <= 2147483647:
18        c_int = obj
19
20    return c_int

【實作層】Cython 優化技巧

使用 cython -a 分析

1# 產生帶註解的 HTML 報告
2cython -a example.pyx
1HTML 報告的顏色含義:
2├── 白色:純 C 程式碼,最快
3├── 淺黃色:少量 Python API 呼叫
4├── 深黃色:較多 Python 互動
5└── 橙色/紅色:大量 Python 操作,需要優化

常見優化模式

 1# 優化前:大量黃色
 2def slow_function(data):
 3    total = 0
 4    for item in data:
 5        total += item * item
 6    return total
 7
 8# 優化後:大部分白色
 9def fast_function(double[:] data):  # 型別化記憶體視圖
10    cdef:
11        int i
12        int n = data.shape[0]
13        double total = 0.0
14
15    for i in range(n):
16        total += data[i] * data[i]
17
18    return total

停用邊界檢查

 1# 預設:有邊界檢查(安全但較慢)
 2cdef double[:] arr = some_array
 3
 4# 停用檢查(確定安全時使用)
 5cimport cython
 6
 7@cython.boundscheck(False)  # 停用邊界檢查
 8@cython.wraparound(False)   # 停用負數索引
 9def optimized_sum(double[:] arr):
10    cdef int i
11    cdef int n = arr.shape[0]
12    cdef double total = 0.0
13
14    for i in range(n):
15        total += arr[i]
16
17    return total
18
19# 或者使用全域設定
20# cython: boundscheck=False
21# cython: wraparound=False

釋放 GIL

 1from cython.parallel import prange
 2
 3# nogil:標記不需要 GIL 的區塊
 4cdef double compute_heavy(double x) nogil:
 5    """純 C 計算,不涉及 Python 物件"""
 6    cdef double result = 0.0
 7    cdef int i
 8    for i in range(1000):
 9        result += x * i
10    return result
11
12def parallel_compute(double[:] data):
13    cdef int i
14    cdef int n = data.shape[0]
15    cdef double[:] results = np.zeros(n)
16
17    # 使用 OpenMP 平行化
18    with nogil:
19        for i in prange(n):
20            results[i] = compute_heavy(data[i])
21
22    return np.asarray(results)

【實作層】與 NumPy 整合

記憶體視圖

 1import numpy as np
 2cimport numpy as cnp
 3
 4# 型別化記憶體視圖(推薦)
 5def process_array(double[:, :] arr):
 6    """處理 2D double 陣列"""
 7    cdef int i, j
 8    cdef int rows = arr.shape[0]
 9    cdef int cols = arr.shape[1]
10    cdef double total = 0.0
11
12    for i in range(rows):
13        for j in range(cols):
14            total += arr[i, j]
15
16    return total
17
18# 使用
19# import numpy as np
20# data = np.random.rand(100, 100)
21# result = process_array(data)

矩陣運算範例

 1# matrix_ops.pyx
 2import numpy as np
 3cimport numpy as cnp
 4cimport cython
 5
 6@cython.boundscheck(False)
 7@cython.wraparound(False)
 8def matrix_multiply(double[:, :] A, double[:, :] B):
 9    """矩陣乘法 C = A @ B"""
10    cdef int i, j, k
11    cdef int m = A.shape[0]
12    cdef int n = A.shape[1]
13    cdef int p = B.shape[1]
14
15    if B.shape[0] != n:
16        raise ValueError("矩陣維度不匹配")
17
18    cdef double[:, :] C = np.zeros((m, p), dtype=np.float64)
19
20    for i in range(m):
21        for j in range(p):
22            for k in range(n):
23                C[i, j] += A[i, k] * B[k, j]
24
25    return np.asarray(C)
26
27# 注意:這只是教學範例
28# 實際應用應使用 numpy.dot 或 BLAS

效能比較

 1import numpy as np
 2import timeit
 3
 4# 假設已編譯 matrix_ops
 5# from matrix_ops import matrix_multiply
 6
 7def benchmark():
 8    A = np.random.rand(100, 100)
 9    B = np.random.rand(100, 100)
10
11    # NumPy(使用 BLAS)
12    t1 = timeit.timeit(lambda: A @ B, number=100)
13
14    # Cython(我們的實現)
15    # t2 = timeit.timeit(lambda: matrix_multiply(A, B), number=100)
16
17    # 純 Python
18    def py_matmul(A, B):
19        m, n = A.shape
20        p = B.shape[1]
21        C = [[0.0] * p for _ in range(m)]
22        for i in range(m):
23            for j in range(p):
24                for k in range(n):
25                    C[i][j] += A[i, k] * B[k, j]
26        return C
27
28    t3 = timeit.timeit(lambda: py_matmul(A, B), number=1)
29
30    print(f"NumPy (BLAS):  {t1:.4f}s")
31    # print(f"Cython:        {t2:.4f}s")
32    print(f"Pure Python:   {t3:.4f}s (x1)")

【實作層】包裝 C 函式庫

宣告外部函式

 1# 宣告 C 標準函式庫函式
 2from libc.math cimport sqrt, sin, cos, pow
 3from libc.stdlib cimport malloc, free
 4from libc.string cimport memcpy, strlen
 5
 6def compute_distance(double x1, double y1, double x2, double y2):
 7    """使用 C 的 sqrt"""
 8    cdef double dx = x2 - x1
 9    cdef double dy = y2 - y1
10    return sqrt(dx * dx + dy * dy)

宣告自訂 C 函式庫

假設有 C 標頭檔 mylib.h

1// mylib.h
2typedef struct {
3    double x, y, z;
4} Vector3D;
5
6double vector_length(Vector3D* v);
7Vector3D vector_add(Vector3D* a, Vector3D* b);

建立 Cython 宣告檔 mylib.pxd

1# mylib.pxd
2cdef extern from "mylib.h":
3    ctypedef struct Vector3D:
4        double x
5        double y
6        double z
7
8    double vector_length(Vector3D* v)
9    Vector3D vector_add(Vector3D* a, Vector3D* b)

使用宣告:

 1# mylib_wrapper.pyx
 2from mylib cimport Vector3D, vector_length, vector_add
 3
 4cdef class PyVector3D:
 5    """Python 包裝類別"""
 6    cdef Vector3D _vec
 7
 8    def __init__(self, double x, double y, double z):
 9        self._vec.x = x
10        self._vec.y = y
11        self._vec.z = z
12
13    @property
14    def x(self):
15        return self._vec.x
16
17    @property
18    def y(self):
19        return self._vec.y
20
21    @property
22    def z(self):
23        return self._vec.z
24
25    def length(self):
26        return vector_length(&self._vec)
27
28    def __add__(self, PyVector3D other):
29        cdef Vector3D result = vector_add(&self._vec, &other._vec)
30        return PyVector3D(result.x, result.y, result.z)
31
32    def __repr__(self):
33        return f"Vector3D({self.x}, {self.y}, {self.z})"

記憶體管理

 1from libc.stdlib cimport malloc, free
 2
 3cdef class DynamicArray:
 4    """管理動態分配記憶體的範例"""
 5    cdef double* data
 6    cdef int size
 7
 8    def __cinit__(self, int size):
 9        """C 層級初始化,保證在 __init__ 之前執行"""
10        self.size = size
11        self.data = <double*>malloc(size * sizeof(double))
12        if self.data == NULL:
13            raise MemoryError("無法分配記憶體")
14
15    def __dealloc__(self):
16        """C 層級解構,保證釋放記憶體"""
17        if self.data != NULL:
18            free(self.data)
19
20    def __init__(self, int size):
21        """Python 層級初始化"""
22        cdef int i
23        for i in range(self.size):
24            self.data[i] = 0.0
25
26    def __getitem__(self, int index):
27        if index < 0 or index >= self.size:
28            raise IndexError("索引超出範圍")
29        return self.data[index]
30
31    def __setitem__(self, int index, double value):
32        if index < 0 or index >= self.size:
33            raise IndexError("索引超出範圍")
34        self.data[index] = value
35
36    def __len__(self):
37        return self.size

【進階】Pure Python 模式

使用型別註解

從 Cython 3.0 開始,支援純 Python 語法:

 1# pure_example.py(純 Python 檔案)
 2import cython
 3
 4@cython.cfunc
 5def c_function(x: cython.int, y: cython.int) -> cython.int:
 6    """等同於 cdef int c_function(int x, int y)"""
 7    return x + y
 8
 9@cython.ccall
10def hybrid_function(x: cython.int) -> cython.int:
11    """等同於 cpdef int hybrid_function(int x)"""
12    return c_function(x, x)
13
14def public_api(n: cython.int) -> cython.long:
15    """普通 Python 函式,但有型別最佳化"""
16    total: cython.long = 0
17    i: cython.int
18
19    for i in range(n):
20        total += hybrid_function(i)
21
22    return total

優點

1Pure Python 模式的好處:
21. 不需要 .pyx 檔案,直接用 .py
32. IDE 支援更好(型別提示)
43. 可以同時作為 Python 和 Cython 使用
54. 測試更容易(不需編譯就能跑 Python)

【建構】現代化建構方式

使用 pyproject.toml

 1# pyproject.toml
 2[build-system]
 3requires = ["setuptools>=61.0", "cython>=3.0"]
 4build-backend = "setuptools.build_meta"
 5
 6[project]
 7name = "my-cython-package"
 8version = "0.1.0"
 9
10[tool.setuptools]
11ext-modules = [
12    {name = "my_module", sources = ["src/my_module.pyx"]}
13]

使用 meson-python

1# pyproject.toml
2[build-system]
3requires = ["meson-python", "cython"]
4build-backend = "mesonpy"
5
6[project]
7name = "my-cython-package"
8version = "0.1.0"
 1# meson.build
 2project('my-cython-package', 'cython')
 3
 4py = import('python').find_installation()
 5
 6py.extension_module(
 7    'my_module',
 8    'src/my_module.pyx',
 9    install: true
10)

思考題

  1. 為什麼 cdef 函式比 def 函式快?從呼叫協議的角度解釋。
  2. 在什麼情況下,Cython 的效能提升最明顯?什麼情況下提升有限?
  3. 如何決定哪些函式應該用 cdef、cpdef 還是 def?

實作練習

  1. 將入門系列效能章節的 is_prime 函式用 Cython 改寫,比較效能差異
  2. 使用 Cython 實現一個簡單的 LRU Cache,與 functools.lru_cache 比較效能
  3. 包裝一個簡單的 C 函式庫(如 zlib)並在 Python 中使用

延伸閱讀


上一章:ctypes 與 cffi 下一章:pybind11