Cython入門教程

Cython Logo

好好的為何要混合Python代碼和C代碼呢?原因主要有2個:

  • Python性能差,將一部分核心邏輯用C語言實現以提升整體性能
  • 希望Python能夠調用一個C語言實現的系統,典型例子:OpenCV計算機視覺庫

Python、C混合編程并不奇怪,Python官方就提供了Python/C API可以實現「用C語言編寫Python庫」,見官方文檔,如果你點開看了你可能就會發現,這好難啊!Python/C API入門門檻太高,于是有了Cython的誕生。

Cython是基于Python/C API的,但學習Cython的時候完全不用了解Python/C API。


Cython和Python/C API

第1章 Cython的安裝和使用

1.1 安裝

在Linux下通過pip install Cython安裝。安裝完畢后執行cython --version,如果輸出了版本號即安裝成功。

1.2 快速入門

本節完整代碼見這里

安裝完成后,我們創建一個Hello World項目,需要創建hello.pyxsetup.py兩個文件。

# file: hello.pyx
def say_hello_to(name):
    print("Hello %s!" % name)
# file: setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(name='Hello world app',
      ext_modules=cythonize("hello.pyx"))

這樣編譯項目:python setup.py build_ext --inplace,會生成hello.so以及一些沒用的中間文件。
下面測試我們生成的hello.so能不能用:

# coding: utf-8
# 這個import會先找hello.py,找不到就會找hello.so
import hello  # 導入了hello.so

hello.say_hello_to('張三')

1.3 Cython實現Python調用C庫

完整代碼見這里

如果我們已經有一個C語言的動態庫、靜態庫,如何在Python中調用外部C庫呢(本節以動態庫為例)?

現有C庫如下,是一個叫做cmath的庫:

// file: cmath.c
#include "cmath.h"
int add(int a, int b)
{
    return a + b;
}
// file: cmath.h
int add(int a, int b);

下面將該cmath封裝為Python庫,為了防止名稱沖突,命名為pymath:

# file: pymath.pyx
cdef extern from "cmath.h":
    int add(int a, int b)

def pyadd(int a, int b):
    return add(a, b)

然后還需要寫setup.py,但這里不想寫setup.py了,因為本文主要使用gcc手工編譯的方式。

1.4 手工gcc編譯

本節完整代碼見這里

本節介紹gcc這種比較原始的編譯方式,是希望你能搞懂Cython如何運作。如果能掌握那么相信在日后的開發工作中各種編譯、部署的問題都不太可能難倒你。

我們知道Ubuntu下Python是這樣安裝的:apt-get install python3,但你可能不知道有這個東西:apt-get install python3-dev
python3-dev這個包安裝的是Python的頭文件,以Ubuntu 18.04為例,安裝完成后你應該可以在/usr/include/python3.6/找到一些頭文件。

看圖1-1可以看到3種方式的對比:

  • 第一條線是用Python/C API,有2個哭臉,不但代碼寫起來煩人,編譯構建也煩人,所以我們才用Cython取代Python/C API;
  • 第二條線是我們最常用的setup.py,有2個笑臉,Cython項目最常用的方式;
  • 第三條線有1個哭臉,也是本節要講的,如何使用gcc這種傳統的方式來編譯Cython項目;
圖1-1 3種方式對比

主要步驟是:

  • 使用cython xxx.pyx生成xxx.c
  • 然后使用gcc -fPIC -shared -I/usr/include/python2.7/ xxx.c -o xxx.so來生成so文件
  • 要注意頭文件版本,自己用的是python2的頭文件還是python3的頭文件

第2章 Cython封裝C庫基礎

2.1 在Cython中調用C庫函數

本節完整代碼見這里

C語言有很多庫函數,例如:

  • libc的atoi函數
  • math庫的sin函數

這些庫函數非常常用,所以Cython已經幫我們封裝了,所以我們直接調用即可。
那么Cython到底幫我們封裝了多少C庫函數呢?你可以在這里找找。
如果你需要調用的函數Cython沒有封裝,那么你需要自己封裝,會在2.2節介紹。

現在我們看下Cython如何調用這些封裝好的C庫函數:

# file: demo.pyx
from libc.math cimport sin
from libc.stdlib cimport atof

def foo(char *s):
    x = atof(s)
    return sin(x)

測試一下可不可以用:

# file: test.py
import demo
print(demo.foo("3.1415"))  # 答案約等于0

2.2 實現Python環境調用C庫函數

本節完整代碼見這里

在2.1節我們已經看到Cython能夠調用C函數,Cython中定義的函數能被Python調用,因此Cython就成為了Python調用C的“橋梁”,我們把這一過程叫做wrap,實現這一功能的Cython代碼叫做wrapper,見圖2-1。通常wrapper可以指一段代碼、一個類,甚至也能泛指一類技術。

圖2-1 wrapper

就和C語言開發一樣,Cython代碼也需要:包含頭文件、鏈接靜態庫/動態庫。

對于這幾個C結構體、函數:

// file: queue.h
typedef struct _Queue Queue;
typedef void *QueueValue;
struct _Queue {
    QueueEntry *head;
    QueueEntry *tail;
};
Queue *queue_new(void);
void queue_free(Queue *queue);

希望在Cython中調用:

# file: queue.pyx
cdef extern from "queue.h":  # 包含頭文件
    ctypedef struct Queue:
        pass
    ctypedef void *QueueValue

    Queue *queue_new()
    void queue_free(Queue *queue)

def foo():
    # 雖然沒有實際意義,但這段代碼很自嗨,可以看到Cython中完全可以調用C函數
    cdef Queue *q
    q = queue_new()
    queue_free(q)

上面代碼看出來雖然Cython可以調用C,但作為wrapper還有一個要求是將C語言自然地封裝成Python風格,所以還需要下面這段代碼讓API更加符合面向對象:

cdef class PyQueue:
    cdef Queue *_c_queue

    def __cinit__(self):
        self._c_queue = queue_new()

    def __dealloc__(self):
        if self._c_queue is not NULL:
            queue_free(self._c_queue)

編譯:

# file: setup.py
from distutils.core import setup, Extension
from Cython.Build import cythonize

extension = Extension(
    "queue",
    ["queue.pyx"],
    libraries=["cqueue"]  # 在這邊聲明需要鏈接的C庫(libcqueue.so)
)

setup(
    ext_modules=cythonize([extension])
)

這里只貼了創建、釋放的封裝。其它功能(如pop、push)見完整代碼。

2.3 回調函數

本節完整代碼見這里

對于一些需要傳入回調函數的接口,會造成調用、被調用關系的反轉。在之前我們討論的都是在Cython中調用C函數,然而回調函數使得問題變為如何讓C調用Cython函數。例如現在希望封裝一個這樣的C函數:

void traverse(int *arr, int len, void (*cb)(int)) {
    for (int i = 0; i < len; i++) {
        cb(arr[i]);
    }
}

為了實現回調的封裝:

  • 首先需要在Cython中定義一個能被C語言調用的wrap_cb,這是容易的
  • 然后需要在Cython的wrap_cb中調用Python的回調函數(我們把它叫做app_cb),這步會比較難實現,因為C環境調用wrap_cb時無法將app_cb的信息傳入

在圖2-2展示的方案中,將app_cb存至全局變量,這樣wrap_cb可以從全局變量取到app_cb

圖2-2 回調函數的封裝

2.4 異步回調

2.3節中提到的方案不適用于異步場景,見下文專門章節分析異步場景。

2.5 結構體的封裝

本節完整代碼見這里

第3章 pxd文件

就像C語言有.c.h文件,Cython有.pyx.pxd文件,可以幫助更好的組織、管理代碼,pxd也可以實現wrapper的復用。

3.1 名稱沖突問題

本節完整代碼見這里

在之前的例子中,我們把C函數的導入、Python wrapper的封裝都放在了pyx文件中,這會導致一些符號名沖突。例如:

cdef extern from "queue.h":
    # 這是聲明C語言中有一個名為Queue的結構體
    ctypedef struct Queue:
        pass

# 這是提供給Python用的類,我們其實也想起名叫做Queue,但C語言結構體也叫這個名字
# 所以我們不得不把提供給Python的類名改為PyQueue
cdef class PyQueue:
    cdef Queue *_c_queue

    def __cinit__(self):
        self._c_queue = ...

為了解決開發中遇到的這些問題,我們可以把聲明放在pxd中,這樣就多了一層命名空間,如下:

# cqueue.pxd
cdef extern from "queue.h":
    ctypedef struct Queue:
        pass

有了命名空間,在pyx中就不會產生符號名沖突了:

# queue.pyx
cimport cqueue
cdef class Queue:
    cdef cqueue.Queue *_c_queue

    def __cinit__(self):
        self._c_queue = ...

3.2 Cython代碼復用

第4章 異步和內存管理

C程序員手動管理內存,而Python得益于垃圾回收機制,程序員無需感知內存管理。

附錄:Cython語法參考

Cython易用的原因是它的代碼跟Python幾乎一樣,Cython的語法是Python的「超集」,即Python代碼一定是Cython代碼,而Cython代碼不一定是Python代碼。比起Python來說,Cython多了一些跟C語言相關的語法。

# Python語法
import math  # 導入math.py或math.so或math目錄
from math import add as myadd  # Python:導入math.py中的add符號,為避免名字沖突,重命名為myadd
math.add(1, 2)  # 訪問math中的add符號
myadd(1, 2)

# 對應的Cython語法
cimport math  # 導入math.pxd
from math cimport add as myadd  # 導入math.pxd中的add符號,為避免名字沖突,重命名為myadd
math.add(1, 2)  # 訪問math中的add符號
myadd(1, 2)
# Python語法
def foo(a, b):  # 定義foo函數
    c = 0  # 創建Python的int對象
    c = a + b
    return c

# Cython語法
cdef int foo(int a, int b):  # cdef是定義C語言函數,注意該函數不能被Python調用
    cdef int c = 0  # 這是C語言的int變量
    c = a + b
    return c  # 返回C語言的int

# Cython語法
cpdef int foo(int a, int b):  # cpdef定義的函數可以被Python調用
    cdef int c = 0  # C語言的int變量
    c = a + b

    # 返回的是Python的int對象
    # Cython在這里隱式將C語言int變量轉為了Python的int對象
    # 因為變量c是基本類型,Cython幫忙轉了,如果c是復雜的是不能直接return的
    return c
# Python語法
class Person():
    def __init__(self):  # 這是構造函數
        pass

# Cython語法
class Person():
    def __init__(self):  # 和C語言相關的內存分配(如malloc)不能放在這里實現
        pass

    def __cinit__(self):  # 和C語言相關的內存分配(如malloc)要放在這里實現 
        ... = malloc();

    def __dealloc__(self):  # 和C語言相關的內存釋放(如free)要放在這里實現 
        free(...);

寫在最后:完整介紹Cython是一個龐大的工程,本文只是介紹了Cython的皮毛,若有疑問歡迎交流。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容