Cython入門教程

Cython Logo

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

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

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

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


Cython和Python/C API

第1章 Cython的安裝和使用

1.1 安裝

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

1.2 快速入門

本節(jié)完整代碼見這里

安裝完成后,我們創(chuàng)建一個Hello World項目,需要創(chuàng)建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實現(xiàn)Python調(diào)用C庫

完整代碼見這里

如果我們已經(jīng)有一個C語言的動態(tài)庫、靜態(tài)庫,如何在Python中調(diào)用外部C庫呢(本節(jié)以動態(tài)庫為例)?

現(xiàn)有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編譯

本節(jié)完整代碼見這里

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

我們知道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個哭臉,不但代碼寫起來煩人,編譯構(gòu)建也煩人,所以我們才用Cython取代Python/C API;
  • 第二條線是我們最常用的setup.py,有2個笑臉,Cython項目最常用的方式;
  • 第三條線有1個哭臉,也是本節(jié)要講的,如何使用gcc這種傳統(tǒng)的方式來編譯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庫基礎(chǔ)

2.1 在Cython中調(diào)用C庫函數(shù)

本節(jié)完整代碼見這里

C語言有很多庫函數(shù),例如:

  • libc的atoi函數(shù)
  • math庫的sin函數(shù)

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

現(xiàn)在我們看下Cython如何調(diào)用這些封裝好的C庫函數(shù):

# 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 實現(xiàn)Python環(huán)境調(diào)用C庫函數(shù)

本節(jié)完整代碼見這里。

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

圖2-1 wrapper

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

對于這幾個C結(jié)構(gòu)體、函數(shù):

// 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中調(diào)用:

# 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中完全可以調(diào)用C函數(shù)
    cdef Queue *q
    q = queue_new()
    queue_free(q)

上面代碼看出來雖然Cython可以調(diào)用C,但作為wrapper還有一個要求是將C語言自然地封裝成Python風格,所以還需要下面這段代碼讓API更加符合面向?qū)ο螅?/p>

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])
)

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

2.3 回調(diào)函數(shù)

本節(jié)完整代碼見這里。

對于一些需要傳入回調(diào)函數(shù)的接口,會造成調(diào)用、被調(diào)用關(guān)系的反轉(zhuǎn)。在之前我們討論的都是在Cython中調(diào)用C函數(shù),然而回調(diào)函數(shù)使得問題變?yōu)槿绾巫孋調(diào)用Cython函數(shù)。例如現(xiàn)在希望封裝一個這樣的C函數(shù):

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

為了實現(xiàn)回調(diào)的封裝:

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

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

圖2-2 回調(diào)函數(shù)的封裝

2.4 異步回調(diào)

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

2.5 結(jié)構(gòu)體的封裝

本節(jié)完整代碼見這里。

第3章 pxd文件

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

3.1 名稱沖突問題

本節(jié)完整代碼見這里

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

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

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

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

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

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

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

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

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

3.2 Cython代碼復用

第4章 異步和內(nèi)存管理

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

附錄:Cython語法參考

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

# 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函數(shù)
    c = 0  # 創(chuàng)建Python的int對象
    c = a + b
    return c

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

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

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

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容