常量
我们在之前的文章中提到,Cython 理解 const 修饰符,但它在 cdef 声明中并不是有效的。它应该在 cdef extern from 语句块中使用,用来修饰一个函数的参数或者返回值。
假设在 header.h 中有这样一段声明:
typedef const int *const_int_ptr; const double *returns_ptr_to_const(const_int_ptr);
在 Cython 中就可以这么写:
cdef extern from "header.h": ctypedef const int* const_int_ptr const double *returns_ptr_to_const(const_int_ptr)
可以看到声明真的非常类似,基本上没太大改动,只需要将 typedef 换成 ctypedef、并将结尾的分号去掉即可。但事实上即使分号不去掉在 Cython 中也是合法的,只不过这不是符合 Cython 风格的代码。
C 里面除了 const 还有 volatile 和 restrict,但这两个在 Cython 中是不合法的。
然后 const 除了可以在 cdef extern from 语句块里面,还可以出现在外部函数的参数和返回值类型声明当中:
cdef const int* func(const int* p): pass
注意:我们在定义一个普通的静态变量时,不可以使用 const,因此它能出现的位置非常有限。说实话,const 对于 Cython 而言,意义不大。
给 C 变量起别名
在 Cython 中,偶尔为 C 的变量起别名是很有用的,这允许我们可以在 Cython 中以不同的名称引用一个 C 级对象,怎么理解呢?举个例子:
// header.h unsigned long __return_int(unsigned long); // source.c unsigned long __return_int(unsigned long n) { return n + 1; }
C 函数前面带了两个下划线,我们看着别扭,再或者它和 Python 的某个内置函数、关键字发生冲突等等,这个时候我们需要为其指定一个别名。
# cython_test.pyx cdef extern from "header.h": # 在 C 中定义的是 __return_int # 但这里我们为其起了一个别名 return_int unsigned long return_int "__return_int"(unsigned long) # 然后直接通过别名进行调用 def py_return_int(n): return return_int(n)
我们测试一下,编写编译脚本 setup.py:
from distutils.core import setup, Extension from Cython.Build import cythonize ext = [Extension("cython_test", sources=["cython_test.pyx", "source.c"], include_dirs=["."])] setup(ext_modules=cythonize(ext, language_level=3))
导入测试一下:
import cython_test print(cython_test.py_return_int(123)) # 124
我们看到没有任何问题,Cython 做的还是比较周密的,为我们考虑到了方方面面。并且这里起别名不仅仅可以用于函数,还可以是结构体、枚举、类型别名之类的。举个例子:
// header.h typedef int class; struct del { int a; float b; }; enum yield { ALOT, SOME, ALITTLE };
我们给 int 起了一个别名叫 class,定义了一个结构体 del 和枚举 yield,这些都是 Python 的关键字,我们不能直接用,需要换一个名字。
cdef extern from "header.h": # C 里面的是 class,这里起个别名叫 klass ctypedef int klass "class" # del 是 Python 的关键字,这里换成 _del struct _del "del": int a float b # yield 是 Python 的关键字,这里换成 _yield enum _yield "yield": ALOT SOME ALITTLE cdef klass num = 123 cdef _del s = _del(a=1, b=3.14) print(num) print(s) print(ALOT, SOME, ALITTLE) """ 123 {'a': 1, 'b': 3.140000104904175} 0 1 2 """
执行没有问题,Cython 考虑到了 Python 和 C 在关键字上会有冲突,因此设计了这一语法。冲突了没有关系,换一个名字就可以了,比如 del 是 Python 的关键字,那么就写成 struct _del。但是这么做还不够,因为头文件里面没有定义 _del 这个结构体,所以这么写会报错,我们需要写成 struct _del "del",表示使用的是 C 中的 del,但是我们在 Cython 中换了个名字叫 _del。
在任何情况下,引号里的字符串都是生成的 C 代码中的对象名,而 Cython 不会检查该字符串的内容,因此可以使用(滥用)这一特性来控制 C 一级的声明。
错误检测和引发异常
对于外部的 C 函数而言,如果出现了异常,那么一种常见的做法是返回一个错误的状态码或者错误标志。但这些异常是在 C 中出现的异常,不是在 Cython 中出现的,因此为了正确地表示 C 中出现的异常,我们必须要对其进行包装。当在 C 中出现异常时,显式地将其引发出来。如果不这么做、而只是单纯的异常捕获的话,那么是无效的,因为 Cython 不会对 C 中出现的异常进行检测,所以在 Python 中也是无法进行异常捕获的。
而如果想做到这一点,需要将 except 字句和 cdef 回调一起绑定起来。
我们说过 Cython 支持 C 函数指针,通过这个特性,可以包装一个接收函数指针作为回调的 C 函数。回调函数可以是不调用 Python/C API 的纯 C 函数,也可以调用任意的 Python 代码,这取决于你要实现的功能逻辑,因此这个强大的特性允许我们在运行时通过 cdef 创建一个函数来控制底层 C 函数的行为。
下面举例说明,首先在 C 的标准库 stdlib 中有一个 qsort 函数,用于对数组排序,函数的原型如下:
我们看到里面接收四个参数,含义如下:
- array:数组指针;
- count:数组的元素个数,因为数组在传递的时候会退化为指针,所以无法通过 sizeof 计算元素个数,需要显式传递;
- size:数组元素的大小;
- compare:比较函数,a > b 返回正数、a < b 返回负数、a == b 返回 0;
下面我们就来测试一下,定义一个函数,接收一个列表,然后根据列表创建 C 数组,调用 qsort 对 C 数组排序。排完序之后,再将 C 数组的元素重新设置在列表中,所以整个过程相当于对列表进行排序。
# 因为 stdlib.h 位于标准库中 # 所以加上 <> 可以让编译器直接去标准库中找 # 另外也可以通过 libc.stdlib 进行导入 # from libc.stdlib cimport qsort, malloc, free # 事实上在 stdlib.pxd 里面也是使用了 cdef extern from # 既然 stdlib.pxd 里面已经声明了,那么直接导入也是可以的 cdef extern from "<stdlib.h>": void qsort( void *array, size_t count, size_t size, int (*compare)(const void *, const void *) ) void *malloc(size_t size) void free(void *ptr) # 定义排序函数 cdef int int_compare(const void *a, const void *b): cdef: int ia = (<int *>a)[0] int ib = (<int *>b)[0] return ia - ib # 因为列表支持倒序排序 # 所以我们需要再定义一个倒序排序函数 cdef int int_compare_reverse(const void *a, const void *b): # 直接在正序排序的基础上乘一个 -1 即可 return -int_compare(a, b) # 给一个函数指针起的类型别名 ctypedef int(*qsort_cmp)(const void *, const void *) # 一个包装器, 外界调用的是这个 pyqsort # 在 pyqsort 内部会调用 qsort cpdef pyqsort(list x, bint reverse=False): """ 将 Python 中的列表转成 C 的数组, 用于排序 排序之后再将结果设置到列表中 :param x: 列表 :param reverse: 是否倒序排序 :return: """ cdef: int *array int i, N # 计算列表长度, 并申请对应容量的内存 N = len(x) array = <int *>malloc(sizeof(int) * N) if array == NULL: raise MemoryError("内存不足, 申请失败") # 将列表中的元素拷贝到数组中 for i, val in enumerate(x): array[i] = val # 获取排序函数 cdef qsort_cmp cmp_callback if reverse: cmp_callback = int_compare_reverse else: cmp_callback = int_compare # 调用 C 中的 qsort 函数进行排序 qsort(<void *> array, <size_t> N, sizeof(int), cmp_callback) # 调用 qsort 结束之后, array 就排序好了 # 然后再将排序好的结果设置在列表中 for i in range(N): # 注意: 此时不能对 array 使用 enumerate # 因为它是一个 int * x[i] = array[i] # 此时 Python 中的列表就已经排序好了 # 别忘记最后将 array 释放掉 free(array)
我们说当导入自定义的 C 文件时,应该通过手动编译的方式,否则会找不到相应的文件。但这里我们导入的是标准库中的头文件,具体实现也位于编译器当中,不是我们自己写的,因此可以不用手动编译,直接通过 pyximport 自动编译并导入即可。
import pyximport pyximport.install(language_level=3) import random import cython_test # 我们看到此时的 pyqsort 和 内置函数 一样 # 都属于 built-in function 级别, 是不是很有趣呢 print(cython_test.pyqsort) print(max) print(isinstance) print(getattr) """ <built-in function pyqsort> <built-in function max> <built-in function isinstance> <built-in function getattr> """ # 然后我们来看看结果如何吧, 是不是能起到排序的效果呢 lst = [random.randint(10, 100) for _ in range(10)] print(lst) """ [47, 35, 82, 74, 76, 76, 46, 50, 27, 35] """ # 排序 cython_test.pyqsort(lst) # 再次打印 print(lst) """ [27, 35, 35, 46, 47, 50, 74, 76, 76, 82] """ # 然后倒序排序 cython_test.pyqsort(lst, reverse=True) print(lst) """ [82, 76, 76, 74, 50, 47, 46, 35, 35, 27] """
目前看起来一切顺利,没有任何障碍,而且我们在外部自己实现了一个内置函数,这是非常了不起的。
但如果出现了异常呢?我们目前还没有对异常进行处理,下面将逻辑改一下。
cdef int int_compare_reverse(const void *a, const void *b): # 在用于倒序排序的比较函数中加入一行 [][3], # 故意引发一个索引越界,其它地方完全不变 [][3] return -int_compare(a, b)
然后我们再调用它,看看会有什么现象:
import pyximport pyximport.install(language_level=3) import cython_test lst = [1, 2, 3] # 倒序排序 cython_test.pyqsort(lst, reverse=True) print("正常执行")
输出如下:
我们看到,明明出现了索引越界错误,但是程序居然没有停下来,而是把异常忽略掉了。而每一次排序都需要调用这个函数,所以出现了多次 IndexError,并且最后的 print 还打印了。
显然这个问题我们在前面说过,当返回值是 C 的类型时,函数里面的错误会被忽略掉,因此需要使用 except ? -1 充当哨兵。
cdef extern from "<stdlib.h>": void qsort( void *array, size_t count, size_t size, int (*compare)( const void *, const void * ) except ? -1 ) void *malloc(size_t size) void free(void *ptr) # 定义排序函数 cdef int int_compare(const void *a, const void *b) except ? -1: cdef: int ia = (<int *>a)[0] int ib = (<int *>b)[0] return ia - ib cdef int int_compare_reverse(const void *a, const void *b) except ? -1: [][3] return -int_compare(a, b) # 给一个函数指针起的类型别名 ctypedef int(*qsort_cmp)( const void *, const void * ) except ? -1 # pyqsort 函数的部分不变 # ......
由于 except ? -1 也是函数类型的一部分,所以必须都要声明,然后我们再调用试试。
此时异常就正确地抛出来了,但是我们看到 Cython 在接收到 IndexError 之后,又抛出了一个 SystemError。原因就在于 int_compare_reverse 这个函数不是在 Cython 中调用的,而是作为回调在 C 里面调用的。
所以异常传递真的是非常的不容易,主要是这个异常它不是在 Cython 里面发生的,而是在 C 函数内部执行回调时发生的,也就相当于是在 C 里面发生的。
在 Cython 中定义一个 C 函数的回调函数、并且在 C 函数里面因执行回调而出现了 Python 异常时,还能通过 except ? -1 将异常从 C 传递给 Cython,这个过程真的是走了很长的一段路。
注意:我们这里是 except ? -1,也就是采用 -1 充当的哨兵,但哨兵值的类型应该和返回值类型保持一致。如果返回值类型是 double,那么哨兵值就应该写成 -1.0。或者干脆直接点,写成 except * 也是可以的,无论返回值类型是什么,except * 都是满足的,但是会多一点点开销。