分享

常量、修饰符,以及回调函数

 古明地觉O_o 2022-12-08 发布于北京

常量


我们在之前的文章中提到,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(10100for _ 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 = [123]
# 倒序排序
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 - i
b

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 * 都是满足的,但是会多一点点开销。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多