Python代码一键转Jar包及Java调用Python新姿势


Posted in Python onMarch 10, 2020

需求背景

进击的Python

随着人工智能的兴起,Python这门曾经小众的编程语言可谓是焕发了第二春。

Python代码一键转Jar包及Java调用Python新姿势

以tensorflow、pytorch等为主的机器学习/深度学习的开发框架大行其道,助推了python这门曾经以爬虫见长(python粉别生气)的编程语言在TIOBE编程语言排行榜上一路披荆斩棘,坐上前三甲的宝座,仅次于Java和C,将C++、JavaScript、PHP、C#等一众劲敌斩落马下。

Python代码一键转Jar包及Java调用Python新姿势
Python代码一键转Jar包及Java调用Python新姿势

当然,轩辕君向来是不提倡编程语言之间的竞争对比,每一门语言都有自己的优势和劣势,有自己应用的领域。
另一方面,TIOBE统计的数据也不能代表国内的实际情况,上面的例子只是侧面反映了Python这门语言如今的流行程度。

Java 还是 Python

说回咱们的需求上来,如今在不少的企业中,同时存在Python研发团队和Java研发团队,Python团队负责人工智能算法开发,而Java团队负责算法工程化,将算法能力通过工程化包装提供接口给更上层的应用使用。

可能大家要问了,为什么不直接用Java做AI开发呢?要弄两个团队。其实,现在包括TensorFlow在内的框架都逐渐开始支持Java平台,用Java做AI开发也不是不行(轩辕君的前同事就已经在这样做了),但限于历史原因,做AI开发的人本就不多,而这一些人绝大部分都是Python技术栈入坑,Python的AI开发生态已经建设的相对完善,所以造成了在很多公司中算法团队和工程化团队使用不同的语言。

现在该抛出本文的重要问题:Java工程化团队如何调用Python的算法能力?

答案基本上只有一个:Python通过Django/Flask等框架启动一个Web服务,Java中通过Restful API与之进行交互

上面的方式的确可以解决问题,但随之而来的就是性能问题。尤其是在用户量上升后,大量并发接口访问下,通过网络访问和Python的代码执行速度将成为拖累整个项目的瓶颈。

当然,不差钱的公司可以用硬件堆出性能,一个不行,那就多部署几个Python Web服务。

那除此之外,有没有更实惠的解决方案呢?这就是这篇文章要讨论的问题。

给Python加速

寻找方向

上面的性能瓶颈中,拖累执行速度的原因主要有两个:

  • 通过网络访问,不如直接调用内部模块快
  • Python是解释执行,快不起来

众所周知,Python是一门解释型脚本语言,一般来说,在执行速度上:

解释型语言 < 中间字节码语言 < 本地编译型语言

自然而然,我们要努力的方向也就有两个:

  • 能否不通过网络访问,直接本地调用
  • Python不要解释执行

结合上面的两个点,我们的目标也清晰起来:

将Python代码转换成Java可以直接本地调用的模块

对于Java来说,能够本地调用的有两种:

  • Java代码包
  • Native代码模块

其实我们通常所说的Python指的是CPython,也就是由C语言开发的解释器来解释执行。而除此之外,除了C语言,不少其他编程语言也能够按照Python的语言规范开发出虚拟机来解释执行Python脚本:

  • CPython: C语言编写的解释器
  • Jython: Java编写的解释器
  • IronPython: .NET平台的解释器
  • PyPy: Python自己编写的解释器(鸡生蛋,蛋生鸡)

Jython?

如果能够在JVM中直接执行Python脚本,与Java业务代码的交互自然是最简单不过。但随后的调研发现,这条路很快就被堵死了:

  • 不支持Python3.0以上的语法
  • python源码中若引用的第三方库包含C语言扩展,将无法提供支持,如numpy等

这条路行不通,那还有一条:把Python代码转换成Native代码块,Java通过JNI的接口形式调用。

Python -> Native代码

整体思路

先将Python源代码转换成C代码,之后用GCC编译C代码为二进制模块so/dll,接着进行一次Java Native接口封装,使用Jar打包命令转换成Jar包,然后Java便可以直接调用。

Python代码一键转Jar包及Java调用Python新姿势

流程并不复杂,但要完整实现这个目标,有两个关键问题需要解决:

1.Python代码如何转换成C代码?

终于要轮到本文的主角登场了,将要用到的一个核心工具叫:Cython

请注意,这里的Cython和前面提到的CPython不是一回事。CPython狭义上是指C语言编写的Python解释器,是Windows、Linux下我们默认的Python脚本解释器。

而Cython是Python的一个第三方库,你可以通过pip install Cython进行安装。

官方介绍Cython是一个Python语言规范的超集,它可以将Python+C混合编码的.pyx脚本转换为C代码,主要用于优化Python脚本性能或Python调用C函数库。

听上去有点复杂,也有点绕,不过没关系,get一个核心点即可:Cython能够把Python脚本转换成C代码

来看一个实验:

# FileName: test.py
def test_function():
 print("this is print from python script")

将上述代码通过Cython转化,生成test.c,长这个样子:

另外添加一个main.c,在其中实现C语言的main函数,并调用原python中的函数:

extern void test_function();
int main() {
 test_function();
 return 0;
}

输出结果:

可以正常工作!

2.转换后的C代码如何包装成JNI接口使用

实际动手

1.Python源代码

def logic(param):
 print('this is a logic function')

# 接口函数,导出给Java Native的接口
def JNI_API_TestFunction(param):
 print("enter JNI_API_test_function")
 logic(param)
 print("leave JNI_API_test_function")

2.使用Cython工具转换成C代码

3.编译生成动态库

4.封装为Jar包

准备一个JNI调用的Interface:JNITest.java

public class JNITest {
 native boolean Java_PkgName_module_initModule( );
 native void Java_PkgName_module_uninitModule( );
 native String Java_PkgName_module_TestFunction(String param);
}

这里有3个native方法:

  • initModule: 对应C代码中Java_JNITest_initModule(),主要完成Python初始化
  • uninitModule: 对应C代码中Java_JNITest_uninitModule(),主要完成Python反初始化
  • TestFunction: 对应C代码中的Java_JNITest_TestFunction(),为核心业务接口

接口声明文件+二进制动态库文件准备就绪,开始打包:

jar -cvf JNITest.jar ./JNITest

5.Java调用

关键问题

1.import问题

上面演示的案例只是一个单独的py文件,而实际工作中,我们的项目通常是具有多个py文件,并且这些文件通常是构成了复杂的目录层级,互相之间各种import关系,错综复杂。

Cython这个工具有一个最大的坑在于:经过其处理的文件代码中会丢失代码文件的目录层级信息,如下图所示,C.py转换后的代码和m/C.py生成的代码没有任何区别。

Python代码一键转Jar包及Java调用Python新姿势

这就带来一个非常大的问题:A.py或B.py代码中如果有引用m目录下的C.py模块,目录信息的丢失将导致二者在执行import m.C时报错,找不到对应的模块!

幸运的是,经过实验表明,在上面的图中,如果A、B、C三个模块处于同一级目录下时,import能够正确执行。

轩辕君曾经尝试阅读Cython的源代码,并进行修改,将目录信息进行保留,使得生成后的C代码仍然能够正常import,但限于时间仓促,对Python解释器机理了解不足,在一番尝试之后选择了放弃。

在这个问题上卡了很久,最终选择了一种笨办法:将树形的代码层级目录展开成为平坦的目录结构,就上图中的例子而言,展开后的目录结构变成了

A.py
B.py
m_C.py

单是这样还不够,还需要对A、B中引用到C的地方全部进行修正为对m_C的引用。

这看起来很简单,但实际情况远比这复杂,在Python中,import可不只有import这么简单,有各种各样复杂的形式:

import package
import module
import package.module
import module.class / function
import package.module.class / function
import package.*
import module.*
from module import *
from module import module
from package import *
from package import module
from package.module import class / function
...

除此之外,在代码中还可能存在直接通过模块进行引用的写法。

展开成为平坦结构的代价就是要处理上面所有的情况!轩辕君无奈之下只有出此下策,如果各位大佬有更好的解决方案还望不吝赐教。

2.Python GIL问题

Python转换后的jar包开始用于实际生产中了,但随后发现了一个问题:

每当Java并发数一上去之后,JVM总是不定时出现Crash

随后分析崩溃信息发现,崩溃的地方正是在Native代码中的Python转换后的代码中。

  • 难道是Cython的bug?
  • 转换后的代码有坑?
  • 还是说上面的import修正工作有问题?

Python代码一键转Jar包及Java调用Python新姿势

崩溃的乌云笼罩在头上许久,冷静下来思考:
为什么测试的时候正常没有发现问题,上线之后才会崩溃?

再次翻看崩溃日志,发现在native代码中,发生异常的地方总是在malloc分配内存的地方,难不成内存被破坏了?
又敏锐的发现测试的时候只是完成了功能性测试,并没有进行并发压力测试,而发生崩溃的场景总是在多并发环境中。多线程访问JNI接口,那Native代码将在多个线程上下文中执行。

猛地一个警觉:99%跟Python的GIL锁有关系!

Python代码一键转Jar包及Java调用Python新姿势

众所周知,限于历史原因,Python诞生于上世纪九十年代,彼时多线程的概念还远远没有像今天这样深入人心过,Python作为这个时代的产物一诞生就是一个单线程的产品。

虽然Python也有多线程库,允许创建多个线程,但由于C语言版本的解释器在内存管理上并非线程安全,所以在解释器内部有一个非常重要的锁在制约着Python的多线程,所以所谓多线程实际上也只是大家轮流来占坑。

原来GIL是由解释器在进行调度管理,如今被转成了C代码后,谁来负责管理多线程的安全呢?

由于Python提供了一套供C语言调用的接口,允许在C程序中执行Python脚本,于是翻看这套API的文档,看看能否找到答案。

幸运的是,还真被我找到了:

获取GIL锁:

Python代码一键转Jar包及Java调用Python新姿势

释放GIL锁:
Python代码一键转Jar包及Java调用Python新姿势

在JNI调用入口需要获得GIL锁,接口退出时需要释放GIL锁。

加入GIL锁的控制后,烦人的Crash问题终于得以解决!

测试效果

准备两份一模一样的py文件,同样的一个算法函数,一个通过Flask Web接口访问,(Web服务部署于本地127.0.0.1,尽可能减少网络延时),另一个通过上述过程转换成Jar包。

在Java服务中,分别调用两个接口100次,整个测试工作进行10次,统计执行耗时:

Python代码一键转Jar包及Java调用Python新姿势

上述测试中,为进一步区分网络带来的延迟和代码执行本身的延迟,在算法函数的入口和出口做了计时,在Java执行接口调用前和获得结果的地方也做了计时,这样可以计算出算法执行本身的时间在整个接口调用过程中的占比。

  • 从结果可以看出,通过Web API执行的接口访问,算法本身执行的时间只占到了30%+,大部分的时间用在了网络开销(数据包的收发、Flask框架的调度处理等等)。
  • 而通过JNI接口本地调用,算法的执行时间占到了整个接口执行时间的80%以上,而Java JNI的接口转换过程只占用10%+的时间,有效提升了效率,减少额外时间的浪费。
  • 除此之外,单看算法本身的执行部分,同一份代码,转换成Native代码后的执行时间在300~500μs,而CPython解释执行的时间则在2000~4000μs,同样也是相差悬殊。

总结

本文提供了一种Java调用Python功能的新思路,仅供参考,其成熟度和稳定性还有待商榷,通过HTTP Restful接口访问仍然是跨语言对接的首选。

到此这篇关于Python代码一键转Jar包及Java调用Python新姿势的文章就介绍到这了,更多相关Python转Jar包内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python的类变量和成员变量用法实例教程
Aug 25 Python
详解Python编程中time模块的使用
Nov 20 Python
Python实现基本数据结构中栈的操作示例
Dec 04 Python
Python使用Phantomjs截屏网页的方法
May 17 Python
解决vscode python print 输出窗口中文乱码的问题
Dec 03 Python
python tkinter实现界面切换的示例代码
Jun 14 Python
Numpy之reshape()使用详解
Dec 26 Python
关于Pytorch MaxUnpool2d中size操作方式
Jan 03 Python
python matplotlib中的subplot函数使用详解
Jan 19 Python
Django ORM filter() 的运用详解
May 14 Python
使用keras框架cnn+ctc_loss识别不定长字符图片操作
Jun 29 Python
python中pdb模块实例用法
Jan 15 Python
Python读取VOC中的xml目标框实例
Mar 10 #Python
Python 读取xml数据,cv2裁剪图片实例
Mar 10 #Python
python代码xml转txt实例
Mar 10 #Python
Python切割图片成九宫格的示例代码
Mar 10 #Python
pycharm设置python文件模板信息过程图解
Mar 10 #Python
解析pip安装第三方库但PyCharm中却无法识别的问题及PyCharm安装第三方库的方法教程
Mar 10 #Python
浅析pip安装第三方库及pycharm中导入第三方库的问题
Mar 10 #Python
You might like
PHP中去除换行解决办法小结(PHP_EOL)
2011/11/27 PHP
PHP常用技术文之文件操作和目录操作总结
2014/09/27 PHP
WordPress的文章自动添加关键词及关键词的SEO优化
2016/03/01 PHP
浅谈php中curl、fsockopen的应用
2016/12/10 PHP
Paypal实现循环扣款(订阅)功能
2017/03/23 PHP
PHP实现防止表单重复提交功能【基于token验证】
2018/05/24 PHP
用js实现计算代码行数的简单方法附代码
2007/08/13 Javascript
jQuery+jqmodal弹出窗口实现代码分明
2010/06/14 Javascript
基于jquery的下拉框改变动态添加和删除表格实现代码
2020/09/12 Javascript
jquery的ajax()函数传值中文乱码解决方法介绍
2012/11/08 Javascript
jQuery+css实现图片滚动效果(附源码)
2013/03/18 Javascript
jQuery setTimeout()函数使用方法
2013/04/07 Javascript
JavaScript中的普通函数与构造函数比较
2015/04/07 Javascript
js实现简洁的滑动门菜单(选项卡)效果代码
2015/09/04 Javascript
js验证身份证号有效性并提示对应信息
2015/10/19 Javascript
基于JQuery实现图片上传预览与删除操作
2016/05/24 Javascript
javascript 利用arguments实现可变长参数
2016/11/21 Javascript
JavaScript中利用for循环遍历数组
2017/01/15 Javascript
Vue框架之goods组件开发详解
2018/01/25 Javascript
vue 动态表单开发方法案例详解
2019/12/02 Javascript
详解小程序BackgroundAudioManager踩坑之旅
2019/12/08 Javascript
[01:10]DOTA2亚洲邀请赛 征战号角响彻全场
2015/01/06 DOTA
python访问纯真IP数据库的代码
2011/05/19 Python
python实现的一只从百度开始不断搜索的小爬虫
2013/08/13 Python
使用FastCGI部署Python的Django应用的教程
2015/07/22 Python
Django中STATIC_ROOT和STATIC_URL及STATICFILES_DIRS浅析
2018/05/08 Python
PyTorch线性回归和逻辑回归实战示例
2018/05/22 Python
python实现简单http服务器功能
2018/09/17 Python
python单例模式原理与创建方法实例分析
2019/10/26 Python
Python如何使用函数做字典的值
2019/11/30 Python
python同时遍历两个list用法说明
2020/05/02 Python
css3使网页、图片变成灰色兼容大多数浏览器
2014/07/02 HTML / CSS
The Athlete’s Foot新西兰:新西兰最大的运动鞋零售商
2019/12/23 全球购物
关于递归的一道.NET面试题
2013/05/12 面试题
基层党建工作宣传标语
2014/06/24 职场文书
2016年春节慰问信息大全
2015/11/30 职场文书