使用 Cython 构建一个扩展模块看上去很手写扩展有些类似, 因为你需要创建很多包装函数。不过,跟前面不同的是,你不需要在 C 语言中做这些——代码看上去更像是 Python。
作为准备,假设本章介绍部分的示例代码已经被编译到某个叫 libsample 的 C 函数库中了。 首先创建一个名叫 csample.pxd 的文件,如下所示:
# 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 *)
这个文件在 Cython 中的作用就跟 C 的头文件一样。 初始声明 cdef extern from "sample.h" 指定了所需的 C 头文件。 接下来的声明都是来自于那个头文件。文件名是 csample.pxd ,而不是 sample.pxd ——这点很重要。
下一步,创建一个名为 sample.pyx 的问题。 该文件会定义包装器,用来桥接 Python 解释器到 csample.pxd 中声明的 C 代码。
# 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)
cimport csample
def gcd(unsigned int x, unsigned int y):
return csample.gcd(x,y)
对于简单的函数,你并不需要去做太多的事。 Cython 会生成包装代码来正确的转换参数和返回值。 绑定到属性上的 C 数据类型是可选的。不过,如果你包含了它们,你可以另外做一些错误检查。 例如,如果有人使用负数来调用这个函数,会抛出一个异常:
>>> sample.gcd(-10,2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "sample.pyx", line 7, in sample.gcd (sample.c:1284)
def gcd(unsigned int x,unsigned int y):
OverflowError: can't convert negative value to unsigned int
如果你想对包装函数做另外的检查,只需要使用另外的包装代码。例如:
def gcd(unsigned int x, unsigned 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)
处理 Point 结构体一个缺点是它的实现是不可见的。 你不能访问任何属性来查看它的内部。 这里有另外一种方法去包装它,就是定义一个扩展类型,如下所示:
# sample.pyx
cimport csample
from libc.stdlib cimport malloc, free
#...
cdef class Point:
cdef csample.Point *_c_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 函数。