调用第三方库
当我们打算在Java调用python程序的时候,需要考虑什么?
- python实现的功能能否用Java实现?
- 如果是机器学习部分,也许可以考虑使用Deeplearning4j
- 如果涉及较多科学计算,python科学计算常用库比如numpy和pandas底层都用C进行了优化,速度不是劣势。
- 是否将python部分封装为服务?
连接Java和Python的工具有很多,下面列举的只是heatao知道的一部分:
Jython
Website: https://www.jython.org/index
Source: https://github.com/jython
Jython是比较有名的连接Java和Python的桥梁(不应该认为是桥本身),Jython是用Java编写的Python解释器。
Jython最大的劣势在于,目前(2020.10)只支持Python2.7,无法访问庞大的CPython生态系统,不支持如NumPy或SciPy之类的库,而很多机器学习的工具都依赖这些库。
Jython的优势在于灵活,可以直接将python代码嵌入Java代码中,也允许在Python中直接写Java代码:
1 | import org.python.util.PythonInterpreter; |
1 | from java.lang import System # Java import |
Jpy
Website: https://github.com/bcdev/jpy/
Documentation: https://jpy.readthedocs.io/en/latest/
Source: https://github.com/bcdev/jpy
jpy是一个双向的Python-Java桥,可以使用它在Python程序中嵌入Java代码,或者反过来使用。它是专门针对两种语言之间的最大数据传输速度而设计的。
jpy的最初开发是由于需要为一个用Java编程的已建立的科学成像应用程序编写Python扩展,即SNAP toolbox,由欧洲航天局(ESA)资助的项目。(jpy与SNAP发行版捆绑在一起。)
安装:
clone the repository
1 | export JDK_HOME=<your-jdk-dir> |
在Python中使用:
1 | jpy.create_jvm(["-Djava.class.path=" + jar_path]) |
- 需要很多设置才能使用,不是很推荐
Jep(推荐)
Website: https://github.com/ninia/jep
Documentation: https://github.com/ninia/jep/wiki
Source: https://github.com/ninia/jep
在其首页介绍是通过 ==JNI== 来将CPython嵌入Java
试验
setup.py build install命令进行编译安装。
Install完成之后,Python对应的site-package路径也会有对应的jar包。
🌰:
invoke_args.py
1 | import numpy as np |
Java调用Python代码如下,注意把JEP添加到Java的Library Path中去。
1 | package first; |
优点:
- 支持CPython扩展和模块,比如numpy等库,但不能保证在所有情况下都可正常工作
- 支持python3.x
缺点:
- 不支持从Python中调用Java
Javabridge
支持在Python代码中启动JVM虚拟机,方便与Java的API和JVM交互。使用人数不多,可以作为实验或学习使用。
安装
1 | pip3 install javabridge |
🌰:
1 | javabridge.start_vm(run_headless=True, class_path=javabridge.JARS + |
其他的Java与Python交互工具
Py4j
Website: https://www.py4j.org/
允许在Python解释器中运行的Python程序动态访问Java虚拟机中的Java对象
允许Java程序会调Python对象
JPype
是一个Python库,让Python可以使用Java的库
并非像Jython那样实现了解释器,而是通过两个虚拟机的本机接口实现
同时提供对CPython和Java库的访问
PyJNIus
- Website: https://github.com/kivy/pyjnius
- Python库,不支持Java调Python
- 允许Python通过JNI来获得Java的类,从Github star来看是上述几种方式中最多的
- 适用场景:极个别的加密算法等内容,用python不方便实现或者实现较耗时,可基于Pyjnius把java类当做python库使用
通过Runtime调用
在java中,调用系统命令,可以使用:
1 | RunTime.getRuntime().exec() |
该方法会生成一个新的进程去运行调用的程序,这种方式相当于直接对命令行进行操作,可以打开软件,自然也可以调用Python脚本。
🌰:
1 | def add(a,b): |
1 | package first; |
通过runtime调用的缺点:
- 对于复杂数据的返回值,调用交互较为麻烦
- 直接调用没有状态保持,每次调用都是全新的开始
- 跨机远程调用不支持
如果你打算使用Runtime.exec()的方式,需要注意一点Java安全编码喔,见参考:
Python转jar包(不是那么实用)
该方法来自==轩辕之风==大佬的文章,和其他第三方库使用JVM虚拟机的方式不同,这里从JNI入手。
Source: https://juejin.im/post/6844903972596088846#heading-18
使用HTTP接口是一种耦合性较低的方式,也方便Java部分和Python部分单独开发,但问题在于当服务器不够多但访问量较大时,并发网络访问会成为速度的瓶颈。
对于 Java 来说,能够本地调用的有两种:
- Java 代码包
- Native 代码模块
像Jython这种在JVM中使用Python的方式有诸多局限,还有一条路就是,把 Python 代码转换成 Native 代码块,Java 通过 JNI 的接口形式调用。
整体思路
先将 Python 源代码转换成 C 代码,之后用 GCC 编译 C 代码为二进制模块 so/dll,接着进行一次 Java Native 接口封装,使用 Jar 打包命令转换成 Jar 包,然后 Java 便可以直接调用。
其实这个思路很想Jep,如果不想自己开发可以直接考虑Jep的方式。
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 代码
实验
准备 Python 源代码
1 | # FileName: Test.py |
注意1:
这里在 python 源码中使用一种约定:以JNI_API_为前缀开头的函数表示为Python代码模块要导出对外调用的接口函数
,这样做的目的是为了让我们的 Python 一键转 Jar 包系统能自动化识别提取哪些接口作为导出函数。
注意2:
这一类接口函数的输入是一个 python 的 str 类型字符串,输出亦然,如此可便于移植以往通过JSON
形式作为参数的 RESTful 接口。使用JSON
的好处是可以对参数进行封装,支持多种复杂的参数形式,而不用重载出不同的接口函数对外调用。
注意3:
还有一点需要说明的是,在接口函数前缀JNI_API_
的后面,函数命名不能以 python 惯有的下划线命名法,而要使用驼峰命名法,注意这不是建议,而是要求,原因后续会提到。
准备一个 main.c 文件
这个文件的作用是对 Cython 转换生成的代码进行一次封装,封装成 Java JNI 接口形式的风格,以备下一步 Java 的使用。
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
这个文件中一共有3个函数:
- Java_Test_initModule: python初始化工作
- Java_Test_uninitModule: python反初始化工作
- Java_Test_testFunction: 真正的业务接口,封装了对原来Python中定义对JNI_API_testFuncion函数的调用,同时要负责JNI层面的参数jstring类型的转换。
根据 JNI 接口规范,native 层面的 C 函数命名需要符合如下的形式:
1 | // QualifiedClassName: 全类名 |
所以在main.c文件中对定义需要向上面这样命名,这也是为什么前面强调python接口函数命名不能用下划线,这会导致JNI接口找不到对应的native函数。
存在的问题
import问题
上面演示的案例只是一个单独的 py 文件,而实际工作中,我们的项目通常是具有多个 py 文件,并且这些文件通常是构成了复杂的目录层级,互相之间各种 import 关系,错综复杂。
Cython 这个工具有一个最大的坑在于:经过其处理的文件代码中会丢失代码文件的目录层级信息,如下图所示,C.py 转换后的代码和 m/C.py 生成的代码没有任何区别。
这就带来一个非常大的问题:A.py 或 B.py 代码中如果有引用 m 目录下的 C.py 模块,目录信息的丢失将导致二者在执行 import m.C 时报错,找不到对应的模块!
幸运的是,经过实验表明,在上面的图中,如果 A、B、C 三个模块处于同一级目录下时,import 能够正确执行。
Python GIL 问题
众所周知,限于历史原因,Python 诞生于上世纪九十年代,彼时多线程的概念还远远没有像今天这样深入人心过,Python 作为这个时代的产物一诞生就是一个单线程的产品。
虽然 Python 也有多线程库,允许创建多个线程,但由于 C 语言版本的解释器在内存管理上并非线程安全,所以在解释器内部有一个非常重要的锁在制约着 Python 的多线程,所以所谓多线程实际上也只是大家轮流来占坑。
原来 GIL 是由解释器在进行调度管理,如今被转成了 C 代码后,谁来负责管理多线程的安全呢?
获取 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 解释执行的时间则在 20004000μs,同样也是相差悬殊。
缺点
- 如果调用较多,需要事先编写Python转Jar包脚本,并且对于函数的命名等方式有固定要求。
- 在python中,import的形式多种多样,如果全部要求以平坦的结构处理所有情况…
使用GraalVM
Website: https://www.graalvm.org/
Documentation: https://www.graalvm.org/docs/
Source: https://github.com/oracle/graal
GraalVM并非某个库或者工具,而且一个JVM虚拟机,但目前仍处于实验性质,其目标可以用雄心勃勃来形容,引用周志明老师的话:
Graal VM被官方称为“Universal VM”和“Polyglot VM”,这是一个在HotSpot虚拟机基础上增强而成 的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用,这里“任何语言”包括了Java、Scala、 Groovy、Kotlin等基于Java虚拟机之上的语言,还包括了C、C++、Rust等基于LLVM的语言,同时支 持其他像JavaScript、Ruby、Python和R语言等。Graal VM可以无额外开销地混合使用这些编程语言, 支持不同语言中混用对方的接口和对象,也能够支持这些语言使用已经编写好的本地库文件。
Graal VM的基本工作原理是将这些语言的源代码(例如JavaScript)或源代码编译后的中间格式 (例如LLVM字节码)通过解释器转换为能被Graal VM接受的中间表示(Intermediate Representation, IR),譬如设计一个解释器专门对LLVM输出的字节码进行转换来支持C和C++语言,这个过程称为程 序特化(Specialized,也常被称为Partial Evaluation)。Graal VM提供了Truffle工具集来快速构建面向一 种新语言的解释器,并用它构建了一个称为Sulong的高性能LLVM字节码解释器。
参考《深入理解Java虚拟机》周志明
下面这个回答对GraalVM有进一步的介绍:
如何评价 GraalVM 这个项目? - kelthuzadx的回答 - 知乎 https://www.zhihu.com/question/274042223/answer/1270829173
个人看法:
- 目前GraalVM还在实验阶段,无法保证其能兼容Python的现有库,如果希望在工程上同时使用Java和Python,可能尚不适合
- 如果使用Jython可以尝试迁移到GraalVM
我的建议
- 首先需要明白使用的场景
- 如果本地简单实用,那么直接用runtime新开进程就可以
- 尽量减少程序耦合
- 推荐使用RESTFUL,网络传输是其速度的瓶颈
- 使用支持Python3的方式
- 第一个排除Jython,如果希望调用第三方工具,我推荐jep,但并不能保证所有python的库都可以使用
- 如果你希望在Python中使用Java的方法,推荐Pyjnius
参考
https://www.infoworld.com/article/3322898/machine-learning-with-python-an-introduction.html?page=2
https://py.processing.org/tutorials/python-jython-java/
https://talvi.net/a-brief-overview-of-python-java-bridges-in-2020.html
https://juejin.im/post/6844903972596088846#heading-18
https://blog.csdn.net/weixin_42348333/article/details/106169841