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

需求背景

进击的Python

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

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


当然,轩辕君向来是不提倡编程语言之间的竞争对比,每一门语言都有自己的优势和劣势,有自己应用的领域。
另一方面,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便可以直接调用。

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

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生成的代码没有任何区别。

这就带来一个非常大的问题: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修正工作有问题?

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

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

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

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

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

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

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

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

获取GIL锁:

释放GIL锁:

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

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

测试效果

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

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

上述测试中,为进一步区分网络带来的延迟和代码执行本身的延迟,在算法函数的入口和出口做了计时,在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包内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java调用python的方法(jython)

    1 什么是jython? 他其实是一门语言,并非是Java 或者Python的解释器.用它可以实现,java和python代码的互相访问. 2 简单的例子 java中执行python 语句 PythonInterpreter interpreter = new PythonInterpreter(); interpreter.exec("days=('mod','Tue','Wed','Thu','Fri','Sat','Sun'); "); interpreter.exec(&quo

  • python如何使用jt400.jar包代码实例

    这篇文章主要介绍了python如何使用jt400.jar包代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 代码如下 jt400helper.py #coding=utf-8 import jpype import os class JT400Helper(object): def __init__(self, server,username,pwd): jvmpath=r"C:\Program Files\Java\jre1.8.0_6

  • 汇总java调用python方法

    本文为大家分享了java调用python方法,供大家参考,具体内容如下 一.在java类中直接执行python语句 import org.python.util.PythonInterpreter; public class FirstJavaScript { public static void main(String args[]) { PythonInterpreter interpreter = new PythonInterpreter(); interpreter.exec("day

  • 将Python代码打包为jar软件的简单方法

    py 写东西快 但是java 生态广 比如大数据 py 虽然好 但是利用不到java的整个的生态的代码 scala 虽然也好但是毕竟 有些库 需要自己写的多 虽然也很简单 ,但是查文档也很麻烦 那么 问题来了 最简单的的方式就是直接把py 打包 jar 那么 问题又来了 py 打包成java 挺麻烦的 官方文档看不懂 答案 有了 写了个 包 https://github.com/yishenggudou/jythontools 搞这个事情 timger-mac:test timger$ pyth

  • 用python解压分析jar包实例

    写这个玩意的背景:在u8多渠道打包里,需要分析jar包,并把里面的文件按目录和类型分别放在root和assets文件夹里,之前师兄都是手动解压,一个一个文件夹找文件,效率比较低,刚好最近手上的android项目已经做完了,就决定写一个自动化分析jar文件并复制粘贴到指定文件夹的脚本. # -*- coding: utf-8 -*- import os import shutil import zipfile count = 1 def getSumDir(): sumfilelist = os.

  • python调用java的jar包方法

    如下所示: from jpype import * jvmPath = getDefaultJVMPath() jars = ["./Firstmaven-1.0-SNAPSHOT-jar-with-dependencies.jar"] jvm_cp = "-Djava.class.path={}".format(":".join(jars)) startJVM(jvmPath,jvm_cp) sedisObj = JClass("Lo

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

    需求背景 进击的Python 随着人工智能的兴起,Python这门曾经小众的编程语言可谓是焕发了第二春. 以tensorflow.pytorch等为主的机器学习/深度学习的开发框架大行其道,助推了python这门曾经以爬虫见长(python粉别生气)的编程语言在TIOBE编程语言排行榜上一路披荆斩棘,坐上前三甲的宝座,仅次于Java和C,将C++.JavaScript.PHP.C#等一众劲敌斩落马下. 当然,轩辕君向来是不提倡编程语言之间的竞争对比,每一门语言都有自己的优势和劣势,有自己应用的领

  • 总结Java调用Python程序方法

    如何使用Java调用Python程序 本文为大家介绍如何java调用python方法,供大家参考. 实际工程项目中可能会用到Java和python两种语言结合进行,这样就会涉及到一个问题,就是怎么用Java程序来调用已经写好的python脚本呢,一共有三种方法可以实现,具体方法分别为大家介绍: 1. 在java类中直接执行python语句 此方法需要引用org.python包,需要下载Jpython.在这里先介绍一下Jpython.下面引入百科的解释: Jython是一种完整的语言,而不是一个J

  • java 一键部署 jar 包和 war 包

    目录 java 一键部署 jar 包和 war 包 一.创建打包命令 gateway-package.bat 二.创建启动文件 gateway.xml 三.一键打包并部署脚本 gateway-deploy.bat 文件内容 四.双击打包部署 bat 文件 gateway-deploy.bat 五.执行 vue 打包并且上传部署 六.安装 7z 压缩工具并配置系统环境变量 七.创建上传部署文件 webConfig.xml 九.双击执行部署 vue-deploy.bat java 一键部署 jar

  • Android打包篇:Android Studio将代码打包成jar包教程

    一.新建一个as项目,再新建一个model模块 然后再app中的build.gradle中添加model的依赖.然后编译项目. 二.编译完成后,打开model下的build--intermediates--bundles目录,目录下有两个文件夹,debug,default,在default文件夹下有一个classess.jar,就是编译完成的jar包, 这里需要主要的是:因为我们使用的 as 版本不一致,所以会导致classess.jar包的目录页会不一样,不过最终的目录还是在build--in

  • 详解在LINUX上部署带有JAR包的JAVA项目

    在LINUX上部署带有JAR包的JAVA项目 首先eclipse上要装上一个小插件,叫做Fat Jar 点击Fat Jar 红框里选上主类点击Next 如图把勾打上 在该路径下找到jar包 通过ftp协议把jar包放在linux服务器下 进入到jar包路径 输入指令 java -jar XXX.jar 运行成功! 注意!!!!!!!!!!!!!!!! 当你断开服务器连接时,工程会停止! 所以要用下面的指令 指令:nohup java -jar XXX.jar 通过指令ps -ef | grep

  • 详解java调用python的几种用法(看这篇就够了)

    java调用python的几种用法如下: 在java类中直接执行python语句 在java类中直接调用本地python脚本 使用Runtime.getRuntime()执行python脚本文件(推荐) 调用python脚本中的函数 准备工作: 创建maven工程,结构如下: 到官网https://www.jython.org/download.html下载Jython的jar包或者在maven的pom.xml文件中加入如下代码: <dependency> <groupId>org

随机推荐