使用cython擴充套件python庫

汗牛充栋發表於2024-11-27

什麼是Cython

Cython 是一種靜態編譯的程式語言,它結合了 Python 的易用性C語言的高效能,並主要用於加速 Python 程式與 C/C++ 整合。它以一種接近 Python 的語法編寫程式碼,並在編譯過程中將其轉換為高效的 C 程式碼,從而提高執行效能。

Cython 的主要用途

  1. 效能最佳化

    • 用於加速計算密集型任務,如科學計算、數值處理和影像處理。
    • 比如將 Python 迴圈轉換為 C 程式碼,減少直譯器的開銷。
  2. 封裝 C/C++ 程式碼

    • 將現有的 C/C++ 庫封裝成 Python 模組,從而讓 Python 程式使用這些高效能的庫。
  3. 擴充套件模組開發

    • 開發自定義的 Python 擴充套件模組,用於特定的高效能任務。
  4. 高效使用 NumPy

    • 提供對 NumPy 的最佳化支援,使用者可以透過靜態型別宣告來加速陣列運算。

Cython 的特點

  • 透過將 Python 程式碼轉譯為 C 程式碼並編譯成共享庫(.so 檔案, windows為.pyd),可以顯著加快程式碼執行速度,特別是在計算密集型任務中。
  • 可以直接呼叫 C/C++ 函式和庫,也可以將現有 Python 程式與 C/C++ 程式碼整合。
  • Cython 支援大部分 Python 語法,同時透過型別宣告增強效能。
  • 支援現有的 Python 包和庫,例如 NumPy,使用者可以在其中透過新增型別註解來進一步最佳化效能。
  • 對於 Python 開發者,學習 Cython 是相對簡單的,因為它的語法非常類似於 Python。

Cython 的工作原理

  1. 使用.pxd檔案宣告介面, 包括python 介面或c/c++介面, 其具體的實現定義在.pyx.c.cpp檔案中。
  2. 編寫介面實現為.pyx.c.cpp中,Cython(.pyx)程式碼語法類似於 Python,可以選擇性地新增型別宣告。
  3. 使用 cythonize 工具將 .pyx 檔案轉換為 C 程式碼,並編譯為共享庫。
  4. 生成的共享庫可以像普通 Python 模組一樣透過 import 使用。

Cython程式碼示例

基本步驟:

  1. 編寫c/c++程式碼, 包括.h或.hpp、.c或.cpp檔案
  2. 編寫pxd檔案宣告介面, .h檔案中的介面, 及.c/cpp中的實現, 或純宣告cython介面。
  3. 編寫pyx檔案, 匯入pxd中的介面進行封裝, 此檔案中的介面,凡是非cdef宣告的函式都可最終被匯出,不論是def/cdef/cpdef的類都會被匯出。
  4. 利用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,
        }
)

相關文章