用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使用os模块的os.walk遍历文件夹示例
Jan 27 Python
用Python实现一个简单的能够上传下载的HTTP服务器
May 05 Python
python根据日期返回星期几的方法
Jul 06 Python
Python爬豆瓣电影实例
Feb 23 Python
Python3实现的简单验证码识别功能示例
May 02 Python
django的ORM模型的实现原理
Mar 04 Python
python flask web服务实现更换默认端口和IP的方法
Jul 26 Python
Python实现多线程/多进程的TCP服务器
Sep 03 Python
Django 解决上传文件时,request.FILES为空的问题
May 20 Python
tensorflow 20:搭网络,导出模型,运行模型的实例
May 26 Python
Python读写压缩文件的方法
Jul 30 Python
Python列表元素删除和remove()方法详解
Jan 04 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
关于查看MSSQL 数据库 用户每个表 占用的空间大小
2013/06/21 PHP
比较strtr, str_replace和preg_replace三个函数的效率
2013/06/26 PHP
基于PHP的简单采集数据入库程序
2014/07/30 PHP
使用YUI+Ant 实现JS CSS压缩
2014/09/02 PHP
如何用PHP来实现一个动态Web服务器
2015/07/29 PHP
php实现阳历阴历互转的方法
2015/10/28 PHP
ThinkPHP框架搭建及常见问题(XAMPP安装失败、Apache/MySQL启动失败)
2016/04/15 PHP
CI框架中redis缓存相关操作文件示例代码
2016/05/17 PHP
php简单统计中文个数的方法
2016/09/30 PHP
利用PHP访问MySql数据库的逻辑操作以及增删改查的实例讲解
2017/08/30 PHP
php合并数组并保留键值的实现方法
2018/03/12 PHP
Laravel 中创建 Zip 压缩文件并提供下载的实现方法
2019/04/02 PHP
遨游,飞飞,IE,空中网 浏览器无提示关闭方法
2011/07/11 Javascript
jquery里的正则表达式说明
2011/08/03 Javascript
jQuery基于当前元素进行下一步的遍历
2014/05/20 Javascript
每天一篇javascript学习小结(Function对象)
2015/11/16 Javascript
学习JavaScript设计模式之享元模式
2016/01/18 Javascript
iscroll.js的上拉下拉刷新时无法回弹的解决方法
2016/02/18 Javascript
Bootstrap源码解读媒体对象、列表组和面板(10)
2016/12/26 Javascript
React/Redux应用使用Async/Await的方法
2017/11/16 Javascript
前端js中的事件循环eventloop机制详解
2019/05/15 Javascript
jquery添加div实现消息聊天框
2020/02/08 jQuery
[06:25]第二届DOTA2亚洲邀请赛主赛事第二天比赛集锦.mp4
2017/04/03 DOTA
浅析Python的web.py框架中url的设定方法
2016/07/11 Python
PyQt5+requests实现车票查询工具
2019/01/21 Python
Python2和3字符编码的区别知识点整理
2019/08/08 Python
python 穷举指定长度的密码例子
2020/04/02 Python
Python 多进程、多线程效率对比
2020/11/19 Python
高街生活方式全球在线商店:AZBRO
2017/08/26 全球购物
荷兰演唱会和体育比赛订票网站:viagogo荷兰
2018/04/08 全球购物
《母鸡》教学反思
2014/02/25 职场文书
公司活动方案范文
2014/03/06 职场文书
纪检监察建议书
2014/05/19 职场文书
走群众路线剖析材料
2014/10/09 职场文书
离职报告格式
2014/11/04 职场文书
Go语言入门exec的基本使用
2022/05/20 Golang