zoukankan      html  css  js  c++  java
  • 『Python CoolBook』Cython

    github地址

    使用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() 的这个例子还展示了如何处理全局解释器锁。

    1. 语句 with nogil: 声明了一个不需要GIL就能执行的代码块。 在这个块中,不能有任何的普通Python对象——只能使用被声明为 cdef 的对象和函数(pxd中的)。
    2. 另外,外部函数必须现实的声明它们能不依赖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文件就会被修改添加很多语句,所以记得备份。

  • 相关阅读:
    五个知识体系之-Linux常用命令学习
    测试职业生涯中,五个知识体系
    英语:真正有效的英语学习心得,把英语当母语学习!(转载)
    侧滑面板(对viewGroup的自定义)
    安卓程序员要拿到5000和1w的薪资,分别需要掌握哪些技术?
    轻巧级记事本的开发
    web.xml 中的listener、 filter、servlet 加载顺序及其详解
    如何向android studio中导入第三方类库
    【NPR】卡通渲染
    线程池原理及其实现
  • 原文地址:https://www.cnblogs.com/hellcat/p/9126012.html
Copyright © 2011-2022 走看看