用Cython加速Python到“起飞”(推荐)

事先声明,标题没有把“Python”错打成“Cython”,因为要讲的就是名为“Cython”的东西。

Cython是让Python脚本支持C语言扩展的编译器,Cython能够将Python+C混合编码的.pyx脚本转换为C代码,主要用于优化Python脚本性能或Python调用C函数库。由于Python固有的性能差的问题,用C扩展Python成为提高Python性能常用方法,Cython算是较为常见的一种扩展方式。

我们可以对比一下业界主流的几种Python扩展支持C语言的方案:

有试用版水印,是因为穷T_T

ctypes是Python标准库支持的方案,直接在Python脚本中导入C的.so库进行调用,简单直接。swig是一个通用的让高级脚本语言扩展支持C的工具,自然也是支持Python的。ctypes没玩过,不做评价。以c语言程序性能为基准的话,cython封装后下降20%,swig封装后下降70%。功能方面,swig对结构体和回调函数都要使用typemap进行手工编写转换规则,typemap规则写起来略复杂,体验不是很好。cython在结构体和回调上也要进行手工编码处理,不过比较简单。

Cython简单实例

我们尝试用Cython,让Python脚本调用C语言写的打印“Hello World”的函数,来熟悉一下Cython的玩法。注:本文全部示例的完整代码见gihub >>> cython_tutorials

/*filename: hello_world.h */
void print_hello_world();
/*filename: hello_world.c */
#include <stdio.h>
#include "hello_world.h"

void print_hello_world()
{
 printf("hello world...");
}

int main(int arch, char *argv[])
{
 print_hello_world();
 return (0);
}
#file: hello_world.pyx

cdef extern from "hello_world.h":
 void print_hello_world()

def cython_print_hello_world():
 print_hello_world()
#filename: Makefile
all: hello_world cython_hello_world

hello_world:
 gcc hello_world.c -c hello_world.c
 gcc hello_world.o -o hello_world 

cython:
 cython cython_hello_world.pyx

cython_hello_world: cython
 gcc cython_hello_world.c -fPIC -c
 gcc -shared -lpython2.7 -o cython_hello_world.so hello_world.o cython_hello_world.o

clean:
 rm -rf hello_world hello_world.o cython_hello_world.so cython_hello_world.c cython_hello_world.o

用Cython扩展C,最重要的就是编写.pyx脚本文件。.pyx脚本是Python调用C的桥梁,.pyx脚本中即能用Python语法写,也可以用类C语法写。

$ make all # 详细的编译过程可以看Makefile中的相关指令
$ python
>>> import cython_hello_world
>>> cython_hello_world.cython_print_hello_world()
hello world...
>>>

可以看到,我们成功的在Python解释器中调用了C语言实现的函数。

Cython的注意事项

所有工具/语言的简单使用都是令人愉快的,但是深入细节就会发现处处“暗藏杀机”。最近是项目需要扩展C底层库给Python调用,所以引入了Cython。实践过程中踩了很多坑,熬了很多夜T_T。遇到了以下几点需要特别注意的点:

  1. .pyx中用cdef定义的东西,除类以外对.py都是不可见的;
  2. .py中是不能操作C类型的,如果想在.py中操作C类型就要在.pyx中从python object转成C类型或者用含有set/get方法的C类型包裹类;
  3. 虽然Cython能对Python的str和C的“char *”之间进行自动类型转换,但是对于“char a[n]”这种固定长度的字符串是无法自动转换的。需要使用Cython的libc.string.strcpy进行显式拷贝;
  4. 回调函数需要用函数包裹,再通过C的“void *”强制转换后才能传入C函数。

1. .pyx中用cdef定义的类型,除类以外对.py都不可见

我们来看一个例子:

#file: invisible.pyx
cdef inline cdef_function():
 print('cdef_function')

def def_function():
 print('def_function')

cdef int cdef_value

def_value = 999

cdef class cdef_class:
 def __init__(self):
  self.value = 1

class def_class:
 def __init__(self):
  self.value = 1
#file: test_visible.py
import invisible

if __name__ == '__main__':
 print('invisible.__dict__', invisible.__dict__)

输出的invisible模块的成员如下:

$ python invisible.py
{
'__builtins__': <module '__builtin__' (built-in)>,
'def_class': <class invisible.def_class at 0x10feed1f0>,
'__file__': '/git/EasonCodeShare/cython_tutorials/invisible-for-py/invisible.so',
'call_all_in_pyx': <built-in function call_all_in_pyx>,
'__pyx_unpickle_cdef_class': <built-in function __pyx_unpickle_cdef_class>,
'__package__': None,
'__test__': {},
'cdef_class': <type 'invisible.cdef_class'>,
'__name__': 'invisible',
'def_value': 999,
'def_function': <built-in function def_function>,
'__doc__': None}

我们在.pyx用cdef定义的函数cdef_function、变量cdef_value都看不到了,只有类cdef_class能可见。所以,使用过程中要注意可见性问题,不要错误的在.py中尝试使用不可见的模块成员。

2. .py传递C结构体类型

Cython扩展C的能力仅限于.pyx脚本中,.py脚本还是只能用纯Python。如果你在C中定义了一个结构,要从Python脚本中传进来就只能在.pyx手工转换一次,或者用包裹类传进来。我们来看一个例子:

/*file: person_info.h */
typedef struct person_info_t
{
 int age;
 char *gender;
}person_info;

void print_person_info(char *name, person_info *info);
//file: person_info.c
#include <stdio.h>
#include "person_info.h"

void print_person_info(char *name, person_info *info)
{
 printf("name: %s, age: %d, gender: %s\n",
   name, info->age, info->gender);
}
#file: cython_person_info.pyx
cdef extern from "person_info.h":
 struct person_info_t:
  int age
  char *gender
 ctypedef person_info_t person_info

 void print_person_info(char *name, person_info *info)

def cyprint_person_info(name, info):
 cdef person_info pinfo
 pinfo.age = info.age
 pinfo.gender = info.gender
 print_person_info(name, &pinfo)

因为“cyprint_person_info”的参数只能是python object,所以我们要在函数中手工编码转换一下类型再调用C函数。

#file: test_person_info.py
from cython_person_info import cyprint_person_info

class person_info(object):
 age = None
 gender = None

if __name__ == '__main__':
 info = person_info()
 info.age = 18
 info.gender = 'male'

 cyprint_person_info('handsome', info)
$ python test_person_info.py
name: handsome, age: 18, gender: male

能正常调用到C函数。可是,这样存在一个问题,如果我们C的结构体字段很多,我们每次从.py脚本调用C函数都要手工编码转换一次类型数据就会很麻烦。还有更好的一个办法就是给C的结构体提供一个包裹类。

#file: cython_person_info.pyx
from libc.stdlib cimport malloc, free
cdef extern from "person_info.h":
 struct person_info_t:
  int age
  char *gender
 ctypedef person_info_t person_info

 void print_person_info(char *name, person_info *info)

def cyprint_person_info(name, person_info_wrap info):
 print_person_info(name, info.ptr)

cdef class person_info_wrap(object):
 cdef person_info *ptr

 def __init__(self):
  self.ptr = <person_info *>malloc(sizeof(person_info))

 def __del__(self):
  free(self.ptr)

 @property
 def age(self):
  return self.ptr.age
 @age.setter
 def age(self, value):
  self.ptr.age = value

 @property
 def gender(self):
  return self.ptr.gender
 @gender.setter
 def gender(self, value):
  self.ptr.gender = value

我们定义了一个“person_info”结构体的包裹类“person_info_wrap”,并提供了成员set/get方法,这样就可以在.py中直接赋值了。减少了在.pyx中转换数据类型的步骤,能有效的提高性能。

#file: test_person_info.py
from cython_person_info import cyprint_person_info, person_info_wrap

if __name__ == '__main__':
 info_wrap = person_info_wrap()
 info_wrap.age = 88
 info_wrap.gender = 'mmmale'

 cyprint_person_info('hhhandsome', info_wrap)
$ python test_person_info.py
name: hhhandsome, age: 88, gender: mmmale

3. python的str传递给C固定长度字符串要用strcpy

正如在C语言中,字符串之间不能直接赋值拷贝,而要使用strcpy复制一样,python的str和C字符串之间也要用cython封装的libc.string.strcpy函数来拷贝。我们稍微修改上一个例子,让person_info结构体的gender成员为16字节长的字符串:

/*file: person_info.h */
typedef struct person_info_t
{
 int age;
 char gender[16];
}person_info;
#file: cython_person_info.pyx
cdef extern from "person_info.h":
  struct person_info_t:
    int age
    char gender[16]
  ctypedef person_info_t person_info
#file: test_person_info.py
from cython_person_info import cyprint_person_info, person_info_wrap

if __name__ == '__main__':
  info_wrap = person_info_wrap()
  info_wrap.age = 88
  info_wrap.gender = 'mmmale'

  cyprint_person_info('hhhandsome', info_wrap)
$ make
$ python test_person_info.py
Traceback (most recent call last):
 File "test_person_info.py", line 7, in <module>
  info_wrap.gender = 'mmmale'
 File "cython_person_info.pyx", line 39, in cython_person_info.person_info_wrap.gender.__set__
  self.ptr.gender = value
 File "stringsource", line 93, in carray.from_py.__Pyx_carray_from_py_char
IndexError: not enough values found during array assignment, expected 16, got 6

cython转换和make时候是没有报错的,运行的时候提示“IndexError: not enough values found during array assignment, expected 16, got 6”,其实就是6字节长的“mmmale”赋值给了person_info结构体的“char gender[16]”成员。我们用strcpy来实现字符串之间的拷贝就ok了。

#file: cython_person_info.pyx
from libc.string cimport strcpy
…… ……
cdef class person_info_wrap(object):
  cdef person_info *ptr
  …… ……
  @property
  def gender(self):
    return self.ptr.gender
  @gender.setter
  def gender(self, value):
    strcpy(self.ptr.gender, value)
$ make
$ python test_person_info.py
name: hhhandsome, age: 88, gender: mmmale

赋值拷贝正常,成功将“mmmale”拷贝给了结构体的gender成员。

4. 用回调函数作为参数的C函数封装

C中的回调函数比较特殊,用户传入回调函数来定制化的处理数据。Cython官方提供了封装带有回调函数参数的例子

//file: cheesefinder.h
typedef void (*cheesefunc)(char *name, void *user_data);
void find_cheeses(cheesefunc user_func, void *user_data);
//file: cheesefinder.c
#include "cheesefinder.h"

static char *cheeses[] = {
 "cheddar",
 "camembert",
 "that runny one",
 0
};

void find_cheeses(cheesefunc user_func, void *user_data) {
 char **p = cheeses;
 while (*p) {
  user_func(*p, user_data);
  ++p;
 }
}
#file: cheese.pyx
cdef extern from "cheesefinder.h":
  ctypedef void (*cheesefunc)(char *name, void *user_data)
  void find_cheeses(cheesefunc user_func, void *user_data)

def find(f):
  find_cheeses(callback, <void*>f)

cdef void callback(char *name, void *f):
  (<object>f)(name.decode('utf-8'))
import cheese

def report_cheese(name):
  print("Found cheese: " + name)

cheese.find(report_cheese)

关键的步骤就是在.pyx中定义一个和C的回调函数相同的回调包裹函数,如上的“cdef void callback(char *name, void *f)”。之后,将.py中的函数作为参数传递给包裹函数,并在包裹函数中转换成函数对象进行调用。

扩展阅读

更进一步的研究Cython可以参考官方文档和相关书籍:

Cython 0.28a0 documentation

Cython A Guide for Python Programmers

Learning Cython Programming

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 详解如何利用Cython为Python代码加速

    引言 通常,在 Python 中写循环(特别是多重循环)非常的慢,在文章 //www.jb51.net/article/133807.htm中,我们的元胞自动机的状态更新函数 update_state 使用了两重循环,所以我们尝试用 Cython 重构该方法. 代码 我们在同文件夹下新建一个 update.pyx 文件,写入如下内容 import numpy as np cimport numpy as np cimport cython DTYPE = np.float ctypedef np

  • 用Cython加速Python到“起飞”(推荐)

    事先声明,标题没有把"Python"错打成"Cython",因为要讲的就是名为"Cython"的东西. Cython是让Python脚本支持C语言扩展的编译器,Cython能够将Python+C混合编码的.pyx脚本转换为C代码,主要用于优化Python脚本性能或Python调用C函数库.由于Python固有的性能差的问题,用C扩展Python成为提高Python性能常用方法,Cython算是较为常见的一种扩展方式. 我们可以对比一下业界主流的几

  • 详解让Python性能起飞的15个技巧

    目录 前言 如何测量程序的执行时间 1.使用map()进行函数映射 2.使用set()求交集 3.使用sort()或sorted()排序 4.使用collections.Counter()计数 5.使用列表推导 6.使用join()连接字符串 7.使用x,y=y,x交换变量 8.使用while1取代whileTrue 9.使用装饰器缓存 10.减少点运算符(.)的使用 11.使用for循环取代while循环 12.使用Numba.jit加速计算 13.使用Numpy矢量化数组 14.使用in检查

  • 使用Pyrex来扩展和加速Python程序的教程

    Pyrex 是一种专门设计用来编写 Python 扩展模块的语言.根据 Pyrex Web 站点的介绍,"它被设计用来在友好易用的高级 Python 世界和凌乱的低级 C 世界之间搭建一个桥梁."虽然几乎所有的 Python 代码都可以作为有效的 Pyrex 代码使用,但是您可以在 Pyrex 代码中添加可选的静态类型声明,从而使得这些声明过的对象以 C 语言的速度运行. 加速 Python 从某种意义上来说,Pyrex 只是不断发展的 Python 类语言系列的一个部分:Jython

  • Cython编译python为so 代码加密示例

    1. 编译出来的so比网上流传的其他方法小很多. 2. language_level  是python的主版本号,如果python版本是2.x,目前的版本Cython需要人工指定language_level. 3. python setup.py build_ext --inplace  执行脚本 4. 以下是代码片段 from distutils.core import Extension, setup from Cython.Build import cythonize from Cytho

  • 10个顶级Python实用库推荐

    为什么我喜欢Python?对于初学者来说,这是一种简单易学的编程语言,另一个原因:大量开箱即用的第三方库,正是23万个由用户提供的软件包使得Python真正强大和流行. 在本文中,我挑选了15个最有用的软件包,介绍它们的功能和特点. 1. Dash Dash 是一个用于构建基于 Web 的应用程序的 Python 库,无需 JavaScript . Dash 同时也是用于创建分析 Web 应用程序的用户界面库.那些使用 Python 进行数据分析.数据挖掘.可视化.建模.仪器控制和报告的人可以立

  • 七种Python代码审查工具推荐

    虽然Python语言可谓目前最为灵活的开发语言之一,但是开发人员往往会滥用其灵活性,甚至会违反相关的标准.因此,Python代码也经常会出现如下常见质量问题: 导入了一些未曾用到的模块 函数在各种调用中缺少参数 缺少适当的格式缩进 在圆括号.方括号或大括号的前后缺少恰当的空格 显然,上述问题不但会影响代码的可读性,而且会使得代码的审查工作变得更为复杂.为此,我们需要通过诸如PyLint或Flake8 之类的静态分析工具来解决此问题,并减少各种可能出现的误报现象. 与此同时,随着软件开发团队规模的

  • 分享8点超级有用的Python编程建议(推荐)

    我们在用Python进行机器学习建模项目的时候,每个人都会有自己的一套项目文件管理的习惯,我自己也有一套方法,是自己曾经踩过的坑总结出来的,现在在这里分享一下给大家,希望多少有些地方可以给大家借鉴.

  • 如何使用Cython对python代码进行加密

    Cython是属于PYTHON的超集,他首先会将PYTHON代码转化成C语言代码,然后通过c编译器生成可执行文件.优势:资源丰富,适合快速开发.翻译成C后速度比较快,在windows环境中用cython加密后的文件后缀是pyd文件,在linux环境中加密后的问题后缀是so文件,下面以linux环境作为演示 环境准备 系统环境:centos 7 Python版本:python3.x 需要的第三方包:cython 加密代码部分 encryption.py from distutils.core im

  • 10款最佳Python开发工具推荐,每一款都是神器

    在人工智能开启的时代,Python作为人工智能的首选语言,前景可以预见.因此学习Python的人越来越多.不过,很多初学Python的小白都一定有过这样的体验,因为没有好用的Python开发工具而吃了很多苦头.工欲善其事必先利其器,这道理只有在实践中才会深有体会.那么初Python用什么工具好?各个开发工具又有哪些特点?本文我就以上的问题,为初学者们介绍一下比较受欢迎且好用的工具. 一.最强终端:Upterm 本来想推荐fish或者zsh,但其实这两个我也主要是贪图自动补全这个特性.最近在用的这

随机推荐