Cython初窥

这篇关于Cython的文章主要是用来阐述什么是Cython,Cython的主要用途是什么。对于Cython的具体用法基本不涉及,因为我觉得了解它的主要用途以及它的优缺点,那么等到有使用场景的时候再来学习一下它的document就可以了。
1. Python的扩展模块(extention module)
我们知道可以用c、c++来扩展Python,这样做的目的就是为了把一些关键功能用更快、更高效的语言(c、c++)来实现,以提高Python程序的运行效率。
下面是一个示例:

#include<Python.h>

static PyObject *fun(PyObject *self, PyObject *args)
{
    int n, i, t = 12;

    if(!PyArg_ParseTuple(args, "i", &n))
    {
        return NULL;
    }
    for(i = 0; i < n; i++)
    {
        t = t + i;
    }

    return Py_BuildValue("i", t);
}


static PyMethodDef ForAddMethods[] = {
    {"fun",  fun, METH_VARARGS, "For loop add."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

PyMODINIT_FUNC
initforadd(void)
{
    (void) Py_InitModule("foradd", ForAddMethods);
}

这个扩展的函数非常简单,当然这个函数最后能比用Python来实现会快多少也难说,如果输入的n很大的话这个c语言版本可能会比纯Python版本要快不少了。
在用c语言作为Python的扩展的时候需要按照固定的格式来编写,还有我认为在用c扩展Python的时候更需要注意的是Python的引用计数问题,一旦对Python/C API不熟悉那么就很有可能会出现内存泄露的情况。在c代码中得程序自己来负责对象的引用管理,比如你调用PyInt_FromLong(12)创建了一个PyObject,那么你就需要记住在什么时候调用Py_XINCREFPy_DECREF来管理对象的引用。
2. 用Cython来生成Python的扩展
Cython是一个用来快速生成Python扩展模块(extention module)的工具,它的语法是Python语言语法和c语言语法的混血。
下面是一个用Python写的foradd功能:

def fun(n):
    t = 12
    i = 0
    while i < n:
        t = t + i
        i += 1
    return t

接着用cython -a test_foradd.pyx命令来生成一个.c和.html文件, 关于Cython的使用大家自行阅读文档吧,在本文中基本就只会用到这一条命令。
生成的test_foradd.c文件就是Cython把test_foradd.pyx”翻译”成的c语言版本,test_foradd.html是一个py代码和c代码对照的页面,可以在页面中看到每条py语句”翻译”成了哪几条c语句。也就是说你可以用Python来写一个需要c语言来实现的扩展模块,然后用Cython可以自动把Python”翻译”成c语言,这样你就无需关注前面我们自己动手用c语言来写Python扩展遇到的问题了。
生成的test_foradd.c的文件内容太多,我把关键的代码摘录如下:

static PyObject *__pyx_int_0;
static PyObject *__pyx_int_12;
static int __Pyx_InitGlobals(void) {
  __pyx_int_0 = PyInt_FromLong(0); 
  __pyx_int_12 = PyInt_FromLong(12);
  return 0;
}
/* Python wrapper */
static PyMethodDef __pyx_mdef_11test_foradd_1fun = {"fun", (PyCFunction)__pyx_pf_11test_foradd_fun, METH_O, 0};
PyMODINIT_FUNC PyInit_test_foradd(void)
{
  if (__Pyx_InitGlobals() < 0) __PYX_ERR(0, 1, __pyx_L1_error)
  __pyx_m = Py_InitModule4("test_foradd", __pyx_methods, 0, 0, PYTHON_API_VERSION); Py_XINCREF(__pyx_m);
}
static PyObject *__pyx_pf_11test_foradd_fun(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_n) {
  PyObject *__pyx_v_t = NULL;
  PyObject *__pyx_v_i = NULL;
  PyObject *__pyx_r = NULL;
  __Pyx_RefNannyDeclarations
  PyObject *__pyx_t_1 = NULL;
  int __pyx_t_2;
  __Pyx_RefNannySetupContext("fun", 0);

  /* "test_foradd.pyx":15
 * 
 * def fun(n):
 *     t = 12             # <<<<<<<<<<<<<<
 *     i = 0
 *     while i < n:
 */
  __Pyx_INCREF(__pyx_int_12);
  __pyx_v_t = __pyx_int_12;

  /* "test_foradd.pyx":16
 * def fun(n):
 *     t = 12
 *     i = 0             # <<<<<<<<<<<<<<
 *     while i < n:
 *         t = t + i
 */
  __Pyx_INCREF(__pyx_int_0);
  __pyx_v_i = __pyx_int_0;

  /* "test_foradd.pyx":17
 *     t = 12
 *     i = 0
 *     while i < n:             # <<<<<<<<<<<<<<
 *         t = t + i
 *         i += 1
 */
  while (1) {
    __pyx_t_1 = PyObject_RichCompare(__pyx_v_i, __pyx_v_n, Py_LT); __Pyx_XGOTREF(__pyx_t_1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)
    __pyx_t_2 = __Pyx_PyObject_IsTrue(__pyx_t_1); if (unlikely(__pyx_t_2 < 0)) __PYX_ERR(0, 17, __pyx_L1_error)
    __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
    if (!__pyx_t_2) break;

    /* "test_foradd.pyx":18
 *     i = 0
 *     while i < n:
 *         t = t + i             # <<<<<<<<<<<<<<
 *         i += 1
 *     return t
 */
    __pyx_t_1 = PyNumber_Add(__pyx_v_t, __pyx_v_i); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 18, __pyx_L1_error)
    __Pyx_GOTREF(__pyx_t_1);
    __Pyx_DECREF_SET(__pyx_v_t, __pyx_t_1);
    __pyx_t_1 = 0;

    /* "test_foradd.pyx":19
 *     while i < n:
 *         t = t + i
 *         i += 1             # <<<<<<<<<<<<<<
 *     return t
 */
    __pyx_t_1 = __Pyx_PyInt_AddObjC(__pyx_v_i, __pyx_int_1, 1, 1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 19, __pyx_L1_error)
    __Pyx_GOTREF(__pyx_t_1);
    __Pyx_DECREF_SET(__pyx_v_i, __pyx_t_1);
    __pyx_t_1 = 0;
  }

  /* "test_foradd.pyx":20
 *         t = t + i
 *         i += 1
 *     return t             # <<<<<<<<<<<<<<
 */
  __Pyx_XDECREF(__pyx_r);
  __Pyx_INCREF(__pyx_v_t);
  __pyx_r = __pyx_v_t;
  goto __pyx_L0;

  /* "test_foradd.pyx":14
 * # ChangeLog:
 * 
 * def fun(n):             # <<<<<<<<<<<<<<
 *     t = 12
 *     i = 0
 */

  /* function exit code */
  __pyx_L1_error:;
  __Pyx_XDECREF(__pyx_t_1);
  __Pyx_AddTraceback("test_foradd.fun", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __pyx_r = NULL;
  __pyx_L0:;
  __Pyx_XDECREF(__pyx_v_t);
  __Pyx_XDECREF(__pyx_v_i);
  __Pyx_XGIVEREF(__pyx_r);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

虽然已经简化了不少代码,但上面的c代码还是看起来有点复杂,主要的原因就是里面的变量命名太随机了。这段c代码的功能跟我们前面自己手写的c扩展代码完成同一个功能,只是一个是”手工”一个是”自动”。
这里写图片描述
代码中的注释很好的解释了每一段代码分别代表了Python中的哪一行代码,还有如果不想看这么长的代码,那么可以直接打开生成的html文件,可以点击每一行带有+的语句,然后就会展示出来”翻译”后的c代码。页面中颜色越yellow的行表示最后”翻译”出来的c代码越多,也可以简单理解为这一行代码在Python中需要执行的opcode越多。
从这个示例来看Cython的”翻译”其实就是把Python代码”翻译”成了等价的c代码(其实也就是调用各种Python/C API),然后不再需要我们来关注c扩展程序的”格式”、各种PyObject对象的引用计数。Cython确实给我们写c扩展模块带来了不少的便利,即使你不懂Python/C API,甚至你可以不会c语言。
3. Dynamic Type(动态类型)和Static Type(静态类型)
C++、Java和C#都是静态语言,它们最大的特点就是变量在使用之前都必须进行类型声明。而Python、JS则是一种动态类型语言,所谓动态,通俗点说就是变量的类型是由最后赋予它的值决定的。
从运行效率来说静态类型是要优于动态类型的,因为在编译的时候就能确定每一个变量的类型,这样编译器就能对编译的结果做一些优化。而动态类型一般都是解释执行的,变量的类型需要在解释运行的时候才能确定,难免会损失一点性能。
对于如下的c代码:

int a = 1;
int b = 2;
int c = a + b;

假设对于+运算符有多种的实现版本(例如有整数版本、浮点数版本、),gcc编译器在编译的时候就知道a、b、c一定是整数类型,对于两个整数的相加那么编译器就可以选择整数版本作为编译的结果(虽然我不知道gcc到底有没有做),那么在运行的时候就省去了一个判断的过程。
相对的,对于如下的Python代码:

a = 1
b = 2
c = a + b

因为Python是动态类型,变量的类型只有在运行时才能确定其类型,上面的a + b这行代码在Python中执行时首先要判断a、b是否属于同一类型,如果不是因为Python属于强类型定义语言那么就会报TypeError: unsupported operand type(s) for +: 'int' and 'str'类似的错误。如果这两个变量是同一类型(整数),那么就会通过PyIntObject->PyObject_HEAD->ob_type获得对应整数类型的类型结构体PyInt_Type,然后再调用PyInt_Type->int_methods->int_add来进行两个整数的相加。这个过程是如此的”冗长”,有兴趣的同学可以看看PyNumber_Add这个函数的实现。
4. Cython的优势
用Cython自动生成的这个c语言扩展比我们手写的扩展代码要复杂很多(其实更多的是看起来复杂),手写几十行代码能搞定的最后Cython却生成了几百行代码,那么这个自动生成的扩展模块能带来性能上的提升吗?答案是:可能会带来性能的提升。
为什么会说可能呢?我们举例的这个是把纯Python代码用Cython来生成扩展模块,代码中没有引入任何Cython的语法,那么Cython就只能”照本宣科”的把每条Python语句”翻译”成对应的c语言版本,因为Python本身是用c语言来实现的而且Python/C API提供了丰富的接口,所以这种等价的”翻译”实现有了可能。
如果你有看过Python的源码那么你会发现Cython的”翻译”结果非常好理解,在Python中i=1这条Python语句就是调用PyInt_FromLong(1)来生成一个PyIntObject
我们这种的”照本宣科”的翻译其实跟纯Python代码在解释器中执行没有太大的区别,所以可能不会带来性能上的提升。那么Cython怎么样才能给我们带来性能的提升呢,答案见下一章节。
5. 进一步的优化
上面这个示例没有引入任何的Cython语法,所以最后带来的性能提升有限,那么我们再看一下进一步优化的版本:

def fun(n):
    cdef int t = 12
    cdef int i = 0
    while i < n:
        t = t + i
    return t

优化后的Python代码中引入了Cython的变量类型定义cdef int来定义一个整数变量,写到这里才发现前面提到的动态类型和静态类型貌似跟这篇文章没有太大的联系,但是我还是加上了,其实也可以理解Cython是混合了动态类型(Python)和静态类型(C语言),我们这次的优化就是把之前一些动态类型的变量变成静态类型的变量。
下面是Cython转换后的c代码:

static int __Pyx_InitGlobals(void) {
  if (__Pyx_InitStrings(__pyx_string_tab) < 0) __PYX_ERR(0, 1, __pyx_L1_error);
  return 0;
  __pyx_L1_error:;
  return -1;
}
static PyObject *__pyx_pf_12test_foradd2_fun(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_n) {
  int __pyx_v_t;
  int __pyx_v_i;
  PyObject *__pyx_r = NULL;
  __Pyx_RefNannyDeclarations
  PyObject *__pyx_t_1 = NULL;
  PyObject *__pyx_t_2 = NULL;
  int __pyx_t_3;
  __Pyx_RefNannySetupContext("fun", 0);

  /* "test_foradd2.pyx":15
 * 
 * def fun(n):
 *     cdef int t = 12             # <<<<<<<<<<<<<<
 *     cdef int i = 0
 *     while i < n:
 */
  __pyx_v_t = 12;

  /* "test_foradd2.pyx":16
 * def fun(n):
 *     cdef int t = 12
 *     cdef int i = 0             # <<<<<<<<<<<<<<
 *     while i < n:
 *         t = t + i
 */
  __pyx_v_i = 0;

  /* "test_foradd2.pyx":17
 *     cdef int t = 12
 *     cdef int i = 0
 *     while i < n:             # <<<<<<<<<<<<<<
 *         t = t + i
 *         i += 1
 */
  while (1) {
    __pyx_t_1 = __Pyx_PyInt_From_int(__pyx_v_i); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)
    __Pyx_GOTREF(__pyx_t_1);
    __pyx_t_2 = PyObject_RichCompare(__pyx_t_1, __pyx_v_n, Py_LT); __Pyx_XGOTREF(__pyx_t_2); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 17, __pyx_L1_error)
    __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
    __pyx_t_3 = __Pyx_PyObject_IsTrue(__pyx_t_2); if (unlikely(__pyx_t_3 < 0)) __PYX_ERR(0, 17, __pyx_L1_error)
    __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
    if (!__pyx_t_3) break;

    /* "test_foradd2.pyx":18
 *     cdef int i = 0
 *     while i < n:
 *         t = t + i             # <<<<<<<<<<<<<<
 *         i += 1
 *     return t
 */
    __pyx_v_t = (__pyx_v_t + __pyx_v_i);

    /* "test_foradd2.pyx":19
 *     while i < n:
 *         t = t + i
 *         i += 1             # <<<<<<<<<<<<<<
 *     return t
 */
    __pyx_v_i = (__pyx_v_i + 1);
  }

  /* "test_foradd2.pyx":20
 *         t = t + i
 *         i += 1
 *     return t             # <<<<<<<<<<<<<<
 */
  __Pyx_XDECREF(__pyx_r);
  __pyx_t_2 = __Pyx_PyInt_From_int(__pyx_v_t); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 20, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_2);
  __pyx_r = __pyx_t_2;
  __pyx_t_2 = 0;
  goto __pyx_L0;

  /* "test_foradd2.pyx":14
 * # ChangeLog:
 * 
 * def fun(n):             # <<<<<<<<<<<<<<
 *     cdef int t = 12
 *     cdef int i = 0
 */

  /* function exit code */
  __pyx_L1_error:;
  __Pyx_XDECREF(__pyx_t_1);
  __Pyx_XDECREF(__pyx_t_2);
  __Pyx_AddTraceback("test_foradd2.fun", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __pyx_r = NULL;
  __pyx_L0:;
  __Pyx_XGIVEREF(__pyx_r);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

这里我只列出了跟章节3的代码有变化的部分,在Python中我们只加入了Cython的cdef int的类型定义,从最后”翻译”的结果来看效果非常明显,最后的c代码中不再出现PyNumber_Add这样的复杂函数,而是全部是简单的c代码。对于用cdef int修饰的变量i、t的所有操作都变成了我们常见的c操作,也无需在__Pyx_InitGlobals中用PyInt_FromLong来创建PyIntObject对象了。
6. 测试对比

def for_add(n):
    i = 0
    while True:
        if i > n:
            break
        i += 1
for_add(10000000)

对比结果如下:
Python:

real 0m0.938s
user 0m0.820s
sys 0m0.008s

Cython(cython test_foradd.pyx):

real 0m0.279s
user 0m0.268s
sys 0m0.008s

Cython(cdef int i):

real 0m0.269s
user 0m0.260s
sys 0m0.008s

从对比的接过来看直接用Cython的效果也还不错,比优化后的Cython没有差太多,从这个对比来看静态编译后确实比动态执行要有一定的优化效果。

【为什么学Python】 Python 是当今非常热门的语言之一,2020年的 TIOBE 编程语言排行榜中 ,Python名列第一,并且其流行度依然处在上升势头。 在2015年的时候,在网上还经常看到学Python还是学R的讨论,那时候老齐就选择了Python,并且开始着手出版《跟老齐学Python》。时至今日,已经无需争论。Python给我们带来的,不仅仅是项目上的收益,我们更可以从它“开放、简洁”哲学观念中得到技术发展路线的启示。 借此机会,老齐联合CSDN推出了本课程,希望能影响更多的人走进Python,踏入编程的大门。 【课程设计】 本课程共包含三大模块: 一、基础知识篇 内置对象和基本的运算、语句,是Python语言的基础。本课程在讲解这部分知识的时候,不是简单地将各种知识做简单的堆砌,而是在兼顾内容的全面性的同时,更重视向学习者讲授掌握有关知识的方法,比如引导学习者如何排查错误、如何查看和理解文档等。   、面向对象篇 “面向对象(OOP)”是目前企业开发主流的开发方式,本课程从一开始就渗透这种思想,并且在“函数”和“类”的学习中强化面向对象开发方式的学习——这是本课程与一般课程的重要区别,一般的课程只在“类”这里才提到“对象”,会导致学习者茫然失措,并生畏惧,乃至于放弃学习。本课程则是从开始以“润物细无声”的方式,渗透对象概念,等学习到本部分的时候,OOP对学习者而言有一种“水到渠成”的感觉。   三、工具实战篇 在项目实战中,除了前述的知识之外,还会用到很多其他工具,至于那些工具如何安装?怎么自己做工具?有那些典型工具?都是这部分的内容。具体来说,就是要在这部分介绍Python标准库的应用以及第三方包的安装,还有如何开发和发布自己的工具包。此外,很多学习Python的同学,未来要么从事数据科学、要么从事Web开发,不论哪个方向,都离不开对数据库的操作,本部分还会从实战的角度,介绍如何用Python语言操作常用数据库。
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页