侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

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

Python  /  管理员 发布于 7年前   166

事先声明,标题没有把“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.pyxcdef extern from "hello_world.h": void print_hello_world()def cython_print_hello_world(): print_hello_world()
#filename: Makefileall: hello_world cython_hello_worldhello_world: gcc hello_world.c -c hello_world.c gcc hello_world.o -o hello_world cython: cython cython_hello_world.pyxcython_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.oclean: 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.pyxcdef inline cdef_function(): print('cdef_function')def def_function(): print('def_function')cdef int cdef_valuedef_value = 999cdef class cdef_class: def __init__(self):  self.value = 1class def_class: def __init__(self):  self.value = 1
#file: test_visible.pyimport invisibleif __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.pyxcdef 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.pyfrom cython_person_info import cyprint_person_infoclass person_info(object): age = None gender = Noneif __name__ == '__main__': info = person_info() info.age = 18 info.gender = 'male'  cyprint_person_info('handsome', info)
$ python test_person_info.pyname: handsome, age: 18, gender: male

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

#file: cython_person_info.pyxfrom libc.stdlib cimport malloc, freecdef 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.pyfrom cython_person_info import cyprint_person_info, person_info_wrapif __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.pyxcdef extern from "person_info.h":  struct person_info_t:    int age    char gender[16]  ctypedef person_info_t person_info
#file: test_person_info.pyfrom cython_person_info import cyprint_person_info, person_info_wrapif __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_charIndexError: 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.pyxfrom 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.htypedef 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.pyxcdef 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 cheesedef 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爬虫刷访问量 2019 7月
    下一条:
    Python爬取视频(其实是一篇福利)过程解析
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在python语言中Flask框架的学习及简单功能示例(0个评论)
    • 在Python语言中实现GUI全屏倒计时代码示例(0个评论)
    • Python + zipfile库实现zip文件解压自动化脚本示例(0个评论)
    • python爬虫BeautifulSoup快速抓取网站图片(1个评论)
    • vscode 配置 python3开发环境的方法(0个评论)
    • 近期文章
    • 智能合约Solidity学习CryptoZombie第四课:僵尸作战系统(0个评论)
    • 智能合约Solidity学习CryptoZombie第三课:组建僵尸军队(高级Solidity理论)(0个评论)
    • 智能合约Solidity学习CryptoZombie第二课:让你的僵尸猎食(0个评论)
    • 智能合约Solidity学习CryptoZombie第一课:生成一只你的僵尸(0个评论)
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2018-04
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2022-01
    • 2023-07
    • 2023-10
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客