什麼是Cython
Cython 是一種靜態編譯的程式語言,它結合了 Python 的易用性和 C語言的高效能,並主要用於加速 Python 程式和與 C/C++ 整合。它以一種接近 Python 的語法編寫程式碼,並在編譯過程中將其轉換為高效的 C 程式碼,從而提高執行效能。
Cython 的主要用途
-
效能最佳化:
- 用於加速計算密集型任務,如科學計算、數值處理和影像處理。
- 比如將 Python 迴圈轉換為 C 程式碼,減少直譯器的開銷。
-
封裝 C/C++ 程式碼:
- 將現有的 C/C++ 庫封裝成 Python 模組,從而讓 Python 程式使用這些高效能的庫。
-
擴充套件模組開發:
- 開發自定義的 Python 擴充套件模組,用於特定的高效能任務。
-
高效使用 NumPy:
- 提供對 NumPy 的最佳化支援,使用者可以透過靜態型別宣告來加速陣列運算。
Cython 的特點
- 透過將 Python 程式碼轉譯為 C 程式碼並編譯成共享庫(
.so
檔案, windows為.pyd
),可以顯著加快程式碼執行速度,特別是在計算密集型任務中。 - 可以直接呼叫 C/C++ 函式和庫,也可以將現有 Python 程式與 C/C++ 程式碼整合。
- Cython 支援大部分 Python 語法,同時透過型別宣告增強效能。
- 支援現有的 Python 包和庫,例如 NumPy,使用者可以在其中透過新增型別註解來進一步最佳化效能。
- 對於 Python 開發者,學習 Cython 是相對簡單的,因為它的語法非常類似於 Python。
Cython 的工作原理
- 使用
.pxd
檔案宣告介面, 包括python 介面或c/c++介面, 其具體的實現定義在.pyx
或.c
、.cpp
檔案中。 - 編寫介面實現為
.pyx
或.c
、.cpp
中,Cython(.pyx)程式碼語法類似於 Python,可以選擇性地新增型別宣告。 - 使用
cythonize
工具將.pyx
檔案轉換為 C 程式碼,並編譯為共享庫。 - 生成的共享庫可以像普通 Python 模組一樣透過
import
使用。
Cython程式碼示例
基本步驟:
- 編寫c/c++程式碼, 包括.h或.hpp、.c或.cpp檔案
- 編寫pxd檔案宣告介面, .h檔案中的介面, 及.c/cpp中的實現, 或純宣告cython介面。
- 編寫pyx檔案, 匯入pxd中的介面進行封裝, 此檔案中的介面,凡是非cdef宣告的函式都可最終被匯出,不論是def/cdef/cpdef的類都會被匯出。
- 利用cythonize工具編譯生成so或pyd
pxd檔案相當於c/c++中的標頭檔案, 主要用於宣告介面及資料型別, 多個pyx檔案可匯入相同的pxd檔案。 另一個作用就是封裝c/c++庫。
匯出C/C++介面
編寫.h及.c檔案:
// test.h
void helloworld();
// test.c
#include <stdio.h>
void helloworld()
{
printf("hello world");
}
再編寫對應hpp/cpp檔案:
// test1.hpp
#include <string>
namespace test{
std::string hello();
}
// test1.cpp
#include "test1.hpp"
std::string test::hello(){
return "hello world";
}
test.pxd
檔案宣告, 函式後面新增except +
表示捕獲異常為python 異常。
from libcpp.string cimport string
# 宣告c
cdef extern from "test.c":
pass
cdef extern from "test.h":
void helloworld() except +
# 宣告c++
cdef extern from "test1.cpp":
pass
cdef extern from "test1.hpp" namespace "test":
string hello() except +
# 宣告cython, 多個pyx可呼叫
cdef void quicksort(double[:] arr, int left, int right)
也可以將C++的宣告放置到另一個pxd中。
編寫對應的pyx檔案, 此檔案主要起到實現或包裝作用:
# distutils: language = c
# cython: language_level=3
import test
# 此介面才會暴露給python
def helloc():
test.helloworld()
# 此介面才會暴露給python
def hellocpp():
test.hello()
# quicksort的實現
cdef void quicksort(double[:] arr, int left, int right):
"""
內部實現快速排序,僅供 Cython 使用
"""
if left >= right:
return
cdef double pivot = arr[left]
cdef int i = left
cdef int j = right
while i < j:
while i < j and arr[j] >= pivot:
j -= 1
arr[i] = arr[j]
while i < j and arr[i] <= pivot:
i += 1
arr[j] = arr[i]
arr[i] = pivot
quicksort(arr, left, i - 1)
quicksort(arr, i + 1, right)
# 此介面才會暴露給python
cpdef sort_array(double[:] arr):
"""
對陣列進行排序,供 Python 呼叫
"""
cdef int n = arr.shape[0]
quicksort(arr, 0, n - 1)
編寫對應的setup.py:
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import sys, os
# c/c++依賴的庫目錄
library_path = ""
include_path = os.path.join(library_path, "include")
libpath = os.path.join(library_path, "lib")
extensions = [
Extension("helloworld", ["test.pyx"],
include_dirs=[include_path],
libraries=libraries,
library_dirs=[libpath])
]
setup(
ext_modules=cythonize(extensions)
)
執行命令編譯:
python setup.py build_ext --inplace
型別的問題
c/c++中的列舉型別與python中的Enum是不對應的, 與python中的int及bint對應。
其他資料型別對應參考連結Using C++ in Cython
多個pyx編譯到一個module中
cython的限制是每定義一個Extension
就會生成一個so或者pyd, 所以預設情況下Extension
的pyx檔案就類似c++中的對外介面hpp檔案, 所有的介面都宣告在這個檔案中。 不支援多個pyx檔案。
多個pyx檔案只能宣告在不同的Extension
中。
如果想一個Extension
中引用多個pyx檔案, 需要將所有pyx檔案include "xxx.pyx"
到一個pyx中, 然後Extension
中僅僅引用包含了所有pyx檔案的pyx, 如下所示的all.pyx中包含了所有pyx:
# 1.pyx
def test1():
pass
# 2.pyx
def test2():
pass
# all.pyx
include "1.pyx"
include "2.pyx"
編譯到指定目錄
編譯到當前目錄:
python setup.py build_ext --inplace
編譯到指定目錄目錄:
python setup.py build_ext -b /dir/to/pyd
如果想要在打包whl檔案時,將cython編譯到指定目錄, 就需要自定義編譯類:
# setup.py
from setuptools.command.build_ext import build_ext
extensions = [
Extension(...)
]
class CustomBuildExt(build_ext):
"""自定義擴充套件模組構建類"""
def get_ext_filename(self, ext_name):
"""
設定pyd安裝目錄
"""
return os.path.join("dir", super().get_ext_filename(ext_name))
setup(
ext_modules=cythonize(extensions),
cmdclass={
'build_ext': CustomBuildExt,
}
)