Cython官方文档中文翻译:使用C库

  • Cython官方文档中文翻译:使用C库

  • 说明

    尝试翻译Cython Documentation以助学习。

    水平有限,乐迎指正;文档首页:《Cython官方文档中文翻译

  • 使用C库

    除了代码加速,Cython的另一个应用场景是从Python中调用外部C库。当Cython代码直接编译为C代码,在代码中直接调用C函数就毫不费力了。下面给出一个完整示例展示在Cython中使用(并封装)一个外部C库,包括适当的错误处理和为PythonCython代码设计合适API的考量。

    假设你需要在一个FIFO(First In First Out,先入先出)序列中存储整型数值的高效方式,因为内存至关重要,数据来自C代码,而你又不能在一个list/deque中创建并存储Python int对象。所以,你就要寻找用C实现的队列。

    经历一番网络搜索,你找到了一个C算法库 [CAlg],决定用它的双端队列实现( double ended queue implementation)。为简化操作,你决定将他封装到一个Python扩展类型中,此扩展可以压缩封装所有内存管理。

    [CAlg]Simon Howard, C Algorithms library, http://c-algorithms.sourceforge.net/

  • 定义外部声明

    此处下载CAIg

    定义在头文件c-algorithms/src/queue.h的这种序列实现的C API,内核看起来如下:

    /* queue.h*/
    typedef struct _Queue Queue;
    typedef void *QueueValue;
    
    Queue *queue_new(void);
    void queue_free(Queue *queue);
    
    int queue_push_head(Queue *queue, QueueValue data);
    QueueValue queue_pop_head(Queue *queue);
    QueueValue queue_peek_head(Queue *queue);
    
    int queue_push_tail(Queue *queue, QueueValue data);
    QueueValue queue_pop_tail(Queue *queue);
    QueueValue queue_peek_tail(Queue *queue);
    
    int queue_is_empty(Queue *queue);
    

    第一步是在一个.pyd文件中重定义C API,比如cqueue.pxd

    # cqueue.pxd
    cdef extern from "c-alpgorithms/src/queue.h":
        ctypedef struct Queue:
            pass
       	ctypedef void* QueueValue
        
        Queue* queue_new()
        void queue_free(Queue* queue)
        
        int queue_push_head(Queue* queue, QueueValue data)
        QueueValue queue_pop_head(Queue* queue)
        QueueValue queue_peek_head(Queue* queue)
        
        int queue_push_tail(Queue* queue, QueueValue data)
        QueueValue queue_pop_tail(Queue* queue)
        QueueValue queue_peek_tail(Queue* queue)
        
        bint queue_is_empty(Queue* queue)
    

    这些声明与头文件声明几近相同,所以很多时候你可以直接复制。实际上,你并不需要提供上述所有声明,只需要提供你代码或其他声明中需要的部分就可以了,因此Cython可以看到他们充分且一致的子集。然后,考虑适应在Cython中使用他们时更合适。

    特别地,你应该为C函数仔细挑选足够好的参数名,因为Cython允许你以关键字参数传递他们。后续改变他们时一个后向不兼容的API修改。选择好的名字会使来自Cython代码的这些函数更好用。

    与头文件相比,我们上面用的第一行中的Queue结构声明有重要不同。本例中,Queue用作一个不透明句柄,只有调用的库知道内部具体是什么。因为Cython代码不知道结构,我们不需要对其内容进行声明,只是提供一个空定义(因为我们不想声明C header中提到的*_Queue类型*)1

    另一个异常是最后一行,queue_is_empty()函数返回的整数值实际上是一个C布尔值,也就是说它唯一有价值的就在于是否为0,指明序列是否为空。Cythonbint类型最能表现这种情况,这种类型在C中就是一个常规int类型,但是当转换为Python对象时就映射成Python布尔值TrueFalse。这种在*.pxd*文件中绑定声明的方式常常可以简化使用代码。

    为每个库定义一个*.pxd文件是一个好方式,有时API很大甚至是为每一个头文件(或函数组)。这可以简化模型间的调用。有时你会用到C标准库中的C函数*,或希望从CPython直接调用C-API。就常规需求来说,Cython绑定了一系列可在Cython中直接使用的提供上述声明的标准*.pxd文件,主要的包是cpythonlibclibcppNumpy包也有一个标准.pxd文件即numpy*。在源文件的Cython/Includes文件夹下可查看完整*.pxd*文件列表。

  • 编写封装器类

    声明C库API之后,我们开始设计应该包括C queueQueue类,写在queue.pyx2文件中,下面是Queue 类的开始:

    # queue.pyx
    cimport cqueue
    cdef class Queue:
        cdef cqueue.Queue* _c_queue
        def __cinit__(self):
            self._c_queue = cqueue.queue_new()
    

    上文用__cinit__而非__init__,虽然后者也可以用,但不能保证一定能运行(比如,创建子类却网及调用父类的构造函数)。因为不初始化C指针,通常会导致Python解释器的硬崩溃,Cython提供构建时在考虑调用__init__之前就立即调用的__cinit__,因此是初始化新实例cdef作用域的正确位置。然,__cinit__是在对象构建过程中调用的,self尚未构建完成,必须避免除了给cdef领域赋值之外的任何与self有关的操作。

    也要意识到上述代码没有参数,尽管子类型可能需要接受一些。无参数__cinit__()方法是特例,它不接受传递给构造函数的任何参数,索引它不阻止子类添加参数。如果__cinit__()签名中使用了参数,他们必须与实例化类型类等级中那些已声明的__init__方法相匹配。

  • 内存管理

    在我们实现其他方法之前,先要明白上述实现其实不安全。万一调用queue_new()出错,这段代码将简单的吞下(swallow)错误,程序很可能一会后崩溃。根据queue_new()函数文档介绍,上述操作失败的唯一原因是内存不足(insufficient memory)。那样的话,就会返回NULL,而通常这里是返回一个指向新队列的指针。

    Python解决这个问题的方式是抛出MemoryError3。因此我们可以更改初始化函数如下:

    # queue.pyx
    
    cimport cqueue
    cdef class Queue:
        cdef cqueue.Queue* _c_queue
        
        def __cinit__(self):
            self._c_queue = cqueue.queue_new()
            if self._c_queue is NULL:
                raise MemoryError()
    

    Queue实例不再使用(即所有对其引用已删除),下一步就是清理。为此,CPython提供了一份回调函数,该回调函数由Cython作为一个特殊方法__dealloc__()使用。在我们的案例中,我们只需释放C Queue,当然只有在我们在init方法中成功初始化的前提下。

    def __dealloc__(self):
        if self._c_queue is not NULL:
            cqueue.queue_free(self._c_queue)
    
  • 编译和链接

    现在,我们有了一个可供测试的可用Cython模块。为编译,我们要为distutils配置一个setup.py脚本。下面是编译Cython模块的基础脚本:

    from distutils.core import setup
    from distutils.extension import Extension
    from Cython.Build import cythonize
    setup(
    	ext_modules = cythonize([Extension("queue", ["queue.pyx"])])
    )
    

    针对外部C库构建,我们需要确保Cython能找到必要的库。方式有两种:

    1. 我们告诉distutils哪里找自动编译queue.c实现的c源码
    2. 我们构建并安装C-AIg作为系统库,并动态链接;

    如果其他应用也用到了C-AIg,后者有效。

  • 静态链接

    想要自动构建c代码,需要在queue.pyx文件中包含编译器指令:

    # distutils: sources = c-algorithms/src/queue.c
    # distutils: include_dirs = c-algorithms/src/
    
    cimport cqueue
    
    cdef class Queue:
        cdef cqueue.Queue* _c_queue
        def __cinit__(self):
            self._c_queue = cqueue.queue_new()
            if self._c_queue is NULL:
                raise MemoryError()
        def __dealloc__(self):
            if self._c_queue is not NULL:
                cqueue.queue_free(self._c_queue)
    

    sources编译器指令给出了distutils将要编译及链接(静态地)到最终扩展模块中的C文件所在路径。一般所有相关头文件都在include_dirs中,现在我们可以用下述代码构建工程:

    $ python setup.py biuld_ext -i
    

    之后测试构建是否成功:

    $ python -c 'import queue; Q = queue.Queue()'
    
  • 动态链接

    如果我们将要封装地库已经安装在系统内,动态链接(Dynamic linking)是有用的。要执行动态链接,我们首先需要构建和安装c-alg

    在你的系统上构建c-algorithms

    $ cd c-algorithms
    $ sh autogen.sh
    $ ./configure
    $ make
    

    安装CAIg:

    $ make install
    

    如此之后,应该有/usr/local/lib/libcalg.so

    这个路径是Linux系统上的,其他系统会有区别。故而,在后文中需要依据不同系统上libcalg.solibcalg.dll所在路径调整余下的教程。

    在此方法内,我们需要告诉setup脚本与外部库链接,为此需要扩展setup脚本,从:

    ext_modules = cythonize([Extension("queue", ["queue.pyx"])])
    

    到:

    ext_modules= cythonize([
        Extension("queue", ["queue.pyx"],
                 libraries = ["calg"])
    ])
    

    至此,我们可以构建工程:

    $ python setup.py build_ext -i
    

    如果libcalg没有安装在常规位置,用户可以通过传入合适的C编译器标记提供所需的外部参数:

    CFLAGS="-I/usr/local/otherdir/calg/include"  \
    LDFLAGS="-L/usr/local/otherdir/calg/lib"     \
        python setup.py build_ext -i
    

    运行模型之前,先确保libcalg在LD_LIBRARY_PATH环境变量内,即通过下述设置:

    $ export LD_LIBRARY_PATH = $ LD_LIBRARY_PATH:/usr/local/lib
    

    一旦我们首次编译了模块,就可以导入他并实例化一个新队列(Queue):

    $ export PYTHONPATH=.
    $ python -c 'import queue; Q = queue.Queue()'
    

    至此所谈都是Queue类能做的,下面我们丰富他的功能。

  • 映射功能
  • References


  1. cdef struct Queue:passctypedef struct Queue:pass之间有一个微妙的不同,前者声明一种在C代码中作为struct Queue的类型,而后者在C代码中引用为Queue,这是Cython无法隐藏的一个C语言怪癖。大部分现代C库使用ctypedef这类结构。 ↩︎

  2. .pyx文件的文件名需不同于包含源自C库声明的cqueue.pxd↩︎

  3. MemoryError案例中,创建一个新的异常实例以提示MemoryError实际可能失败,因为我们内存不足。幸运的是,CPython提供了一个C-API函数PyErr_NoMemory()为我们安全的抛出正确异常。如果你正用一个旧版本,你需要从cpython.exc标准库cimport这个C-API函数并直接调用。 ↩︎

发布了753 篇原创文章 · 获赞 1021 · 访问量 54万+

猜你喜欢

转载自blog.csdn.net/The_Time_Runner/article/details/103687533