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


Posted in Python onAugust 01, 2019

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

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

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

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

有试用版水印,是因为穷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

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

Python 相关文章推荐
Python中join和split用法实例
Apr 14 Python
python实现kNN算法
Dec 20 Python
tensorflow构建BP神经网络的方法
Mar 12 Python
python调用xlsxwriter创建xlsx的方法
May 03 Python
点球小游戏python脚本
May 22 Python
基于Python中求和函数sum的用法详解
Jun 28 Python
python利用requests库模拟post请求时json的使用教程
Dec 07 Python
python openvc 裁剪、剪切图片 提取图片的行和列
Sep 19 Python
在django中使用apscheduler 执行计划任务的实现方法
Feb 11 Python
python itsdangerous模块的具体使用方法
Feb 17 Python
keras 自定义loss损失函数,sample在loss上的加权和metric详解
May 23 Python
python进度条显示-tqmd模块的实现示例
Aug 23 Python
Python爬取视频(其实是一篇福利)过程解析
Aug 01 #Python
flask框架jinja2模板与模板继承实例分析
Aug 01 #Python
Win10环境python3.7安装dlib模块趟过的坑
Aug 01 #Python
python爬虫解决验证码的思路及示例
Aug 01 #Python
Django多数据库的实现过程详解
Aug 01 #Python
Python解决pip install时出现的Could not fetch URL问题
Aug 01 #Python
numpy.meshgrid()理解(小结)
Aug 01 #Python
You might like
深入Apache与Nginx的优缺点比较详解
2013/06/17 PHP
ThinkPHP3.1.3版本新特性概述
2014/06/19 PHP
Aster vs KG BO3 第一场2.18
2021/03/10 DOTA
Aster vs KG BO3 第三场2.19
2021/03/10 DOTA
JavaScript学习笔记(十)
2010/01/17 Javascript
javascript中的delete使用详解
2013/04/11 Javascript
jQuery实现炫酷的鼠标轨迹特效
2015/02/01 Javascript
JS制作手机端自适应缩放显示
2015/06/11 Javascript
基于jquery步骤进度条源码分享
2015/11/12 Javascript
Bootstrap每天必学之滚动监听
2016/03/16 Javascript
angularjs 源码解析之injector
2016/08/22 Javascript
Bootstrap Modal对话框如何在关闭时触发事件
2016/12/02 Javascript
discuz表情的JS提取方法分析
2017/03/22 Javascript
JS排序之快速排序详解
2017/04/08 Javascript
JavaScript fetch接口案例解析
2018/08/30 Javascript
详解关于html,css,js三者的加载顺序问题
2019/04/10 Javascript
了解Javascript中函数作为对象的魅力
2019/06/19 Javascript
ES5新增数组的实现方法
2020/05/12 Javascript
Python中unittest用法实例
2014/09/25 Python
python模仿网页版微信发送消息功能
2018/02/24 Python
用OpenCV将视频分解成单帧图片,图片合成视频示例
2019/12/10 Python
Python导入模块包原理及相关注意事项
2020/03/25 Python
Pytorch mask-rcnn 实现细节分享
2020/06/24 Python
CSS3轻松实现清新 Loading 效果的简单实例
2016/06/06 HTML / CSS
HTML5和CSS3实例教程总结(推荐)
2016/07/18 HTML / CSS
英国现代、当代和设计师家具店:Furntastic
2020/07/18 全球购物
酒店实习个人鉴定
2013/12/07 职场文书
硕士研究生求职自荐信范文
2014/03/11 职场文书
我的中国梦口号
2014/06/16 职场文书
医生个人年终总结
2015/02/28 职场文书
2016大学军训通讯稿
2015/11/25 职场文书
2016春季幼儿园大班开学寄语
2015/12/03 职场文书
2016年清明节红领巾广播稿
2015/12/17 职场文书
高中化学教学反思
2016/02/22 职场文书
《猴王出世》教学反思
2016/02/23 职场文书
启动Tomcat时出现大量乱码的解决方法
2021/06/21 Java/Android