使用Cython導入庫的話,需要一下幾個文件:
.c:C函數源碼
.h:C函數頭
.pxd:Cython函數頭
.pyx:包裝函數
setup.py:python
本節示例.c和.h文件同『Python CoolBook』使用ctypes訪問C代碼_下_demo進階即存在sample.c和sample.h兩個源文件。
cdef:Cython函數,只能在Cython中調用,python識別不了這個定義后面的主體,而且它后面也不僅僅接函數,class等均可,def定義的函數可以被Python識別,在pxd和pyx中均可使用。
csample.pxd:Cython頭文件
.pxd文件僅僅只包含C定義(類似.h文件),即相當於將.h文件包裝為Cython的頭。注意,.pxd僅僅是聲明定義,我們此時並未對函數做包裝,這個工作在.pyx中完成。
# csample.pxd
#
# Declarations of "external" C functions and structures
cdef extern from "sample.h":
int gcd(int, int)
bint in_mandel(double, double, int)
int divide(int, int, int *)
double avg(double *, int) nogil
ctypedef struct Point:
double x
double y
double distance(Point *, Point *)
為例對比,我們給出sample.h文件如下:
#ifndef __SAMPLE_H__
#define __SAMPLE_H__
#include <math.h>
#ifdef __cplusplus
extern "C" {
#endif
int gcd(int x, int y);
int in_mandel(double x0, double y0, int n);
int divide(int a, int b, int *remainder);
double avg(double *a, int n);
/* A C data structure */
typedef struct Point {
double x,y;
} Point;
double distance(Point *p1, Point *p2);
#ifdef __cplusplus
}
#endif
#endif
sample.pyx:Cython封裝主體
程序如下,pyx文本中語法和python一致,但是卻可以像C中一樣指定形參類型(也可以不指定),實際上這里是最基礎的包裝,可以看到就是調用了csample包中的函數,我們在后文中可以看到對基本包裝的加強。
# sample.pyx
# Import the low-level C declarations
cimport csample
# Import some functionality from Python and the C stdlib
from cpython.pycapsule cimport *
from libc.stdlib cimport malloc, free
# Wrappers
def gcd(unsigned int x, unsigned int y):
return csample.gcd(x, y)
def in_mandel(x, y, unsigned int n):
return csample.in_mandel(x, y, n)
def divide(x, y):
cdef int rem
quot = csample.divide(x, y, &rem)
return quot, rem
def avg(double[:] a):
cdef:
int sz
double result
sz = a.size
with nogil:
result = csample.avg(<double *> &a[0], sz)
return result
# Destructor for cleaning up Point objects
cdef del_Point(object obj):
pt = <csample.Point *> PyCapsule_GetPointer(obj,"Point")
free(<void *> pt)
# Create a Point object and return as a capsule
def Point(double x,double y):
cdef csample.Point *p
p = <csample.Point *> malloc(sizeof(csample.Point))
if p == NULL:
raise MemoryError("No memory to make a Point")
p.x = x
p.y = y
return PyCapsule_New(<void *>p,"Point",<PyCapsule_Destructor>del_Point)
def distance(p1, p2):
pt1 = <csample.Point *> PyCapsule_GetPointer(p1,"Point")
pt2 = <csample.Point *> PyCapsule_GetPointer(p2,"Point")
return csample.distance(pt1,pt2)
由於很多細節都蘊含在上面代碼中,也涉及很多前面介紹過的高級特性,包括數組操作、包裝隱形指針和釋放GIL,所以下面逐個分析各個函數。
各種情況函數分析
gcd:簡單的數字參數函數
csample.pxd 文件聲明了 int gcd(int, int) 函數, sample.pyx 中的包裝函數如下:
cimport csample
def gcd(unsigned int x, unsigned int y): # <--- 無符號整形
return csample.gcd(x,y)
無符號整型使得在運行中接收到負數會報這一行的錯誤,我們可以修改如下,
# def gcd(unsigned int x, unsigned int y):
# return csample.gcd(x, y)
def gcd(int x, int y):
if x <= 0:
raise ValueError("x must be > 0")
if y <= 0:
raise ValueError("y must be > 0")
return csample.gcd(x,y)

可以看到,這里對Python語句支持的很好。
in_mandel:返回值為0或1(布爾整形)
/* Test if (x0,y0) is in the Mandelbrot set or not */
int in_mandel(double x0, double y0, int n) {
double x=0,y=0,xtemp;
while (n > 0) {
xtemp = x*x - y*y + x0;
y = 2*x*y + y0;
x = xtemp;
n -= 1;
if (x*x + y*y > 4) return 0;
}
return 1;
}
pxd聲明可以指定函數返回類型bint:
bint in_mandel(double, double, int)
divide:形參指針對象
int divide(int a, int b, int *remainder) {
int quot = a / b;
*remainder = a % b;
return quot;
}
python沒法傳遞一個地址,但pyx可以,
def divide(x, y):
cdef int rem
quot = csample.divide(x, y, &rem)
return quot, rem
在這里,rem 變量被顯示的聲明為一個C整型變量。 當它被傳入 divide() 函數的時候,&rem 創建一個跟C一樣的指向它的指針。
avg:形參數組&GIL釋放
/* Average values in an array */
double avg(double *a, int n) {
int i;
double total = 0.0;
for (i = 0; i < n; i++) {
total += a[i];
}
return total / n;
}
avg() 函數的代碼演示了Cython更高級的特性:
def avg(double[:] a):
cdef:
int sz
double result
sz = a.size
with nogil:
result = csample.avg(<double *> &a[0], sz)
return result
首先 def avg(double[:] a) 聲明了 avg() 接受一個一維的雙精度內存視圖。 最驚奇的部分是返回的結果函數可以接受任何兼容的數組對象,包括被numpy創建的。例如:
>>> import array >>> a = array.array('d',[1,2,3]) >>> import numpy >>> b = numpy.array([1., 2., 3.]) >>> import sample >>> sample.avg(a) 2.0 >>> sample.avg(b) 2.0 >>>
在此包裝器中,a.size 和 &a[0] 分別引用數組元素個數和底層指針。 語法 <double *> &a[0] 教你怎樣將指針轉換為不同的類型。 前提是C中的 avg() 接受一個正確類型的指針。 參考下一節關於Cython內存視圖的更高級講述。
除了處理通常的數組外,avg() 的這個例子還展示了如何處理全局解釋器鎖。
- 語句
with nogil:聲明了一個不需要GIL就能執行的代碼塊。 在這個塊中,不能有任何的普通Python對象——只能使用被聲明為cdef的對象和函數(pxd中的)。 - 另外,外部函數必須現實的聲明它們能不依賴GIL就能執行。 因此,在csample.pxd文件中,
avg()被聲明為double avg(double *, int) nogil.
distance、Point:結構體處理
本節使用膠囊對象將Point對象當做隱形指針來處理,pxd中聲明如下,
ctypedef struct Point:
double x
double y
首先,下面的導入被用來引入C函數庫和Python C API中定義的函數:
from cpython.pycapsule cimport * # <---膠囊結構函數庫,直接來自Python C API from libc.stdlib cimport malloc, free
包裝如下,先建立結構體,最后以膠囊形式返回:
# Destructor for cleaning up Point objects
cdef del_Point(object obj):
pt = <csample.Point *> PyCapsule_GetPointer(obj,"Point") # <---膠囊結構提取指針(膠囊結構還原結構體)
free(<void *> pt)
# Create a Point object and return as a capsule
def Point(double x,double y):
cdef csample.Point *p
p = <csample.Point *> malloc(sizeof(csample.Point))
if p == NULL:
raise MemoryError("No memory to make a Point")
p.x = x
p.y = y
return PyCapsule_New(<void *>p,"Point",<PyCapsule_Destructor>del_Point)
函數 del_Point() 和 Point() 使用這個功能來創建一個膠囊對象, 它會包裝一個 Point * 指針。
cdef del_Point() 將 del_Point() 聲明為一個函數, 只能通過Cython訪問,而不能從Python中訪問。 因此,這個函數對外部是不可見的——它被用來當做一個回調函數來清理膠囊分配的內存。 函數調用比如 PyCapsule_New() 、PyCapsule_GetPointer() 直接來自Python C API以同樣的方式被使用。
distance 函數從 Point() 創建的膠囊對象中提取指針,
def distance(p1, p2):
pt1 = <csample.Point *> PyCapsule_GetPointer(p1,"Point")
pt2 = <csample.Point *> PyCapsule_GetPointer(p2,"Point")
return csample.distance(pt1,pt2)
這里要注意的是你不需要擔心異常處理。 如果一個錯誤的對象被傳進來,PyCapsule_GetPointer() 會拋出一個異常, 但是Cython已經知道怎么查找到它,並將它從 distance() 傳遞出去。
處理Point結構體一個缺點是它的實現是不可見的。 你不能訪問任何屬性來查看它的內部。 這里有另外一種方法去包裝它,就是定義一個擴展類型,如下所示:
# sample.pyx
cimport csample
from libc.stdlib cimport malloc, free
...
cdef class Point:
cdef csample.Point *_c_point # 聲明Point結構體
def __cinit__(self, double x, double y): # 初始化過程就是建立一個結構體
self._c_point = <csample.Point *> malloc(sizeof(csample.Point))
self._c_point.x = x
self._c_point.y = y
def __dealloc__(self):
free(self._c_point)
property x: # 方法修飾為屬性
def __get__(self):
return self._c_point.x
def __set__(self, value):
self._c_point.x = value
property y: # 方法修飾為屬性
def __get__(self):
return self._c_point.y
def __set__(self, value):
self._c_point.y = value
def distance(Point p1, Point p2):
return csample.distance(p1._c_point, p2._c_point)
在這里,cdif類 Point 將Point聲明為一個擴展類型。 類屬性 cdef csample.Point *_c_point 聲明了一個實例變量, 擁有一個指向底層Point結構體的指針。 __cinit__() 和 __dealloc__() 方法通過 malloc() 和 free() 創建並銷毀底層C結構體。 x和y屬性的聲明讓你獲取和設置底層結構體的屬性值。 distance() 的包裝器還可以被修改,使得它能接受 Point 擴展類型實例作為參數, 而傳遞底層指針給C函數。
做了這個改變后,你會發現操作Point對象就顯得更加自然了:
>>> import sample >>> p1 = sample.Point(2,3) >>> p2 = sample.Point(4,5) >>> p1 <sample.Point object at 0x100447288> >>> p2 <sample.Point object at 0x1004472a0> >>> p1.x 2.0 >>> p1.y 3.0 >>> sample.distance(p1,p2) 2.8284271247461903 >>>
setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
ext_modules = [
Extension('sample',
['sample.pyx'],
libraries=['sample'],
library_dirs=['.'])]
setup(
name = 'Sample extension module',
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules
)
編譯運行
python setup.py build_ext --inplace
注意,編譯完成后sample.c文件就會被修改添加很多語句,所以記得備份。
