Cython入門(mén)教程

Cython Logo

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

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

Python、C混合編程并不奇怪,Python官方就提供了Python/C API可以實(shí)現(xiàn)「用C語(yǔ)言編寫(xiě)Python庫(kù)」,見(jiàn)官方文檔,如果你點(diǎn)開(kāi)看了你可能就會(huì)發(fā)現(xiàn),這好難??!Python/C API入門(mén)門(mén)檻太高,于是有了Cython的誕生。

Cython是基于Python/C API的,但學(xué)習(xí)Cython的時(shí)候完全不用了解Python/C API。


Cython和Python/C API

第1章 Cython的安裝和使用

1.1 安裝

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

1.2 快速入門(mén)

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

安裝完成后,我們創(chuàng)建一個(gè)Hello World項(xiàng)目,需要?jiǎng)?chuàng)建hello.pyxsetup.py兩個(gè)文件。

# 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"))

這樣編譯項(xiàng)目:python setup.py build_ext --inplace,會(huì)生成hello.so以及一些沒(méi)用的中間文件。
下面測(cè)試我們生成的hello.so能不能用:

# coding: utf-8
# 這個(gè)import會(huì)先找hello.py,找不到就會(huì)找hello.so
import hello  # 導(dǎo)入了hello.so

hello.say_hello_to('張三')

1.3 Cython實(shí)現(xiàn)Python調(diào)用C庫(kù)

完整代碼見(jiàn)這里

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

現(xiàn)有C庫(kù)如下,是一個(gè)叫做cmath的庫(kù):

// 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庫(kù),為了防止名稱沖突,命名為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)

然后還需要寫(xiě)setup.py,但這里不想寫(xiě)setup.py了,因?yàn)楸疚闹饕褂胓cc手工編譯的方式。

1.4 手工gcc編譯

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

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

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

看圖1-1可以看到3種方式的對(duì)比:

  • 第一條線是用Python/C API,有2個(gè)哭臉,不但代碼寫(xiě)起來(lái)煩人,編譯構(gòu)建也煩人,所以我們才用Cython取代Python/C API;
  • 第二條線是我們最常用的setup.py,有2個(gè)笑臉,Cython項(xiàng)目最常用的方式;
  • 第三條線有1個(gè)哭臉,也是本節(jié)要講的,如何使用gcc這種傳統(tǒng)的方式來(lái)編譯Cython項(xiàng)目;
圖1-1 3種方式對(duì)比

主要步驟是:

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

第2章 Cython封裝C庫(kù)基礎(chǔ)

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

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

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

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

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

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

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

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

測(cè)試一下可不可以用:

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

2.2 實(shí)現(xiàn)Python環(huán)境調(diào)用C庫(kù)函數(shù)

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

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

圖2-1 wrapper

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

對(duì)于這幾個(gè)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():
    # 雖然沒(méi)有實(shí)際意義,但這段代碼很自嗨,可以看到Cython中完全可以調(diào)用C函數(shù)
    cdef Queue *q
    q = queue_new()
    queue_free(q)

上面代碼看出來(lái)雖然Cython可以調(diào)用C,但作為wrapper還有一個(gè)要求是將C語(yǔ)言自然地封裝成Python風(fēng)格,所以還需要下面這段代碼讓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庫(kù)(libcqueue.so)
)

setup(
    ext_modules=cythonize([extension])
)

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

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

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

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

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

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

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

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

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

2.4 異步回調(diào)

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

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

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

第3章 pxd文件

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

3.1 名稱沖突問(wèn)題

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

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

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

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

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

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

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

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

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

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

3.2 Cython代碼復(fù)用

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

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

附錄:Cython語(yǔ)法參考

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

# Python語(yǔ)法
import math  # 導(dǎo)入math.py或math.so或math目錄
from math import add as myadd  # Python:導(dǎo)入math.py中的add符號(hào),為避免名字沖突,重命名為myadd
math.add(1, 2)  # 訪問(wèn)math中的add符號(hào)
myadd(1, 2)

# 對(duì)應(yīng)的Cython語(yǔ)法
cimport math  # 導(dǎo)入math.pxd
from math cimport add as myadd  # 導(dǎo)入math.pxd中的add符號(hào),為避免名字沖突,重命名為myadd
math.add(1, 2)  # 訪問(wèn)math中的add符號(hào)
myadd(1, 2)
# Python語(yǔ)法
def foo(a, b):  # 定義foo函數(shù)
    c = 0  # 創(chuàng)建Python的int對(duì)象
    c = a + b
    return c

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

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

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

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

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

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

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

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

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

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