Python代碼一鍵轉(zhuǎn)Jar包及Java調(diào)用Python新姿勢(shì)
需求背景
進(jìn)擊的Python
隨著人工智能的興起,Python這門(mén)曾經(jīng)小眾的編程語(yǔ)言可謂是煥發(fā)了第二春。

以tensorflow、pytorch等為主的機(jī)器學(xué)習(xí)/深度學(xué)習(xí)的開(kāi)發(fā)框架大行其道,助推了python這門(mén)曾經(jīng)以爬蟲(chóng)見(jiàn)長(zhǎng)(python粉別生氣)的編程語(yǔ)言在TIOBE編程語(yǔ)言排行榜上一路披荊斬棘,坐上前三甲的寶座,僅次于Java和C,將C++、JavaScript、PHP、C#等一眾勁敵斬落馬下。


當(dāng)然,軒轅君向來(lái)是不提倡編程語(yǔ)言之間的競(jìng)爭(zhēng)對(duì)比,每一門(mén)語(yǔ)言都有自己的優(yōu)勢(shì)和劣勢(shì),有自己應(yīng)用的領(lǐng)域。
另一方面,TIOBE統(tǒng)計(jì)的數(shù)據(jù)也不能代表國(guó)內(nèi)的實(shí)際情況,上面的例子只是側(cè)面反映了Python這門(mén)語(yǔ)言如今的流行程度。
Java 還是 Python
說(shuō)回咱們的需求上來(lái),如今在不少的企業(yè)中,同時(shí)存在Python研發(fā)團(tuán)隊(duì)和Java研發(fā)團(tuán)隊(duì),Python團(tuán)隊(duì)負(fù)責(zé)人工智能算法開(kāi)發(fā),而Java團(tuán)隊(duì)負(fù)責(zé)算法工程化,將算法能力通過(guò)工程化包裝提供接口給更上層的應(yīng)用使用。
可能大家要問(wèn)了,為什么不直接用Java做AI開(kāi)發(fā)呢?要弄兩個(gè)團(tuán)隊(duì)。其實(shí),現(xiàn)在包括TensorFlow在內(nèi)的框架都逐漸開(kāi)始支持Java平臺(tái),用Java做AI開(kāi)發(fā)也不是不行(軒轅君的前同事就已經(jīng)在這樣做了),但限于歷史原因,做AI開(kāi)發(fā)的人本就不多,而這一些人絕大部分都是Python技術(shù)棧入坑,Python的AI開(kāi)發(fā)生態(tài)已經(jīng)建設(shè)的相對(duì)完善,所以造成了在很多公司中算法團(tuán)隊(duì)和工程化團(tuán)隊(duì)使用不同的語(yǔ)言。
現(xiàn)在該拋出本文的重要問(wèn)題:Java工程化團(tuán)隊(duì)如何調(diào)用Python的算法能力?
答案基本上只有一個(gè):Python通過(guò)Django/Flask等框架啟動(dòng)一個(gè)Web服務(wù),Java中通過(guò)Restful API與之進(jìn)行交互
上面的方式的確可以解決問(wèn)題,但隨之而來(lái)的就是性能問(wèn)題。尤其是在用戶(hù)量上升后,大量并發(fā)接口訪(fǎng)問(wèn)下,通過(guò)網(wǎng)絡(luò)訪(fǎng)問(wèn)和Python的代碼執(zhí)行速度將成為拖累整個(gè)項(xiàng)目的瓶頸。
當(dāng)然,不差錢(qián)的公司可以用硬件堆出性能,一個(gè)不行,那就多部署幾個(gè)Python Web服務(wù)。
那除此之外,有沒(méi)有更實(shí)惠的解決方案呢?這就是這篇文章要討論的問(wèn)題。
給Python加速
尋找方向
上面的性能瓶頸中,拖累執(zhí)行速度的原因主要有兩個(gè):
- 通過(guò)網(wǎng)絡(luò)訪(fǎng)問(wèn),不如直接調(diào)用內(nèi)部模塊快
- Python是解釋執(zhí)行,快不起來(lái)
眾所周知,Python是一門(mén)解釋型腳本語(yǔ)言,一般來(lái)說(shuō),在執(zhí)行速度上:
解釋型語(yǔ)言 < 中間字節(jié)碼語(yǔ)言 < 本地編譯型語(yǔ)言
自然而然,我們要努力的方向也就有兩個(gè):
- 能否不通過(guò)網(wǎng)絡(luò)訪(fǎng)問(wèn),直接本地調(diào)用
- Python不要解釋執(zhí)行
結(jié)合上面的兩個(gè)點(diǎn),我們的目標(biāo)也清晰起來(lái):
將Python代碼轉(zhuǎn)換成Java可以直接本地調(diào)用的模塊
對(duì)于Java來(lái)說(shuō),能夠本地調(diào)用的有兩種:
- Java代碼包
- Native代碼模塊
其實(shí)我們通常所說(shuō)的Python指的是CPython,也就是由C語(yǔ)言開(kāi)發(fā)的解釋器來(lái)解釋執(zhí)行。而除此之外,除了C語(yǔ)言,不少其他編程語(yǔ)言也能夠按照Python的語(yǔ)言規(guī)范開(kāi)發(fā)出虛擬機(jī)來(lái)解釋執(zhí)行Python腳本:
- CPython: C語(yǔ)言編寫(xiě)的解釋器
- Jython: Java編寫(xiě)的解釋器
- IronPython: .NET平臺(tái)的解釋器
- PyPy: Python自己編寫(xiě)的解釋器(雞生蛋,蛋生雞)
Jython?
如果能夠在JVM中直接執(zhí)行Python腳本,與Java業(yè)務(wù)代碼的交互自然是最簡(jiǎn)單不過(guò)。但隨后的調(diào)研發(fā)現(xiàn),這條路很快就被堵死了:
- 不支持Python3.0以上的語(yǔ)法
- python源碼中若引用的第三方庫(kù)包含C語(yǔ)言擴(kuò)展,將無(wú)法提供支持,如numpy等
這條路行不通,那還有一條:把Python代碼轉(zhuǎn)換成Native代碼塊,Java通過(guò)JNI的接口形式調(diào)用。
Python -> Native代碼
整體思路
先將Python源代碼轉(zhuǎn)換成C代碼,之后用GCC編譯C代碼為二進(jìn)制模塊so/dll,接著進(jìn)行一次Java Native接口封裝,使用Jar打包命令轉(zhuǎn)換成Jar包,然后Java便可以直接調(diào)用。

流程并不復(fù)雜,但要完整實(shí)現(xiàn)這個(gè)目標(biāo),有兩個(gè)關(guān)鍵問(wèn)題需要解決:
1.Python代碼如何轉(zhuǎn)換成C代碼?
終于要輪到本文的主角登場(chǎng)了,將要用到的一個(gè)核心工具叫:Cython
請(qǐng)注意,這里的Cython和前面提到的CPython不是一回事。CPython狹義上是指C語(yǔ)言編寫(xiě)的Python解釋器,是Windows、Linux下我們默認(rèn)的Python腳本解釋器。
而Cython是Python的一個(gè)第三方庫(kù),你可以通過(guò)pip install Cython進(jìn)行安裝。
官方介紹Cython是一個(gè)Python語(yǔ)言規(guī)范的超集,它可以將Python+C混合編碼的.pyx腳本轉(zhuǎn)換為C代碼,主要用于優(yōu)化Python腳本性能或Python調(diào)用C函數(shù)庫(kù)。
聽(tīng)上去有點(diǎn)復(fù)雜,也有點(diǎn)繞,不過(guò)沒(méi)關(guān)系,get一個(gè)核心點(diǎn)即可:Cython能夠把Python腳本轉(zhuǎn)換成C代碼
來(lái)看一個(gè)實(shí)驗(yàn):
# FileName: test.py
def test_function():
print("this is print from python script")
將上述代碼通過(guò)Cython轉(zhuǎn)化,生成test.c,長(zhǎng)這個(gè)樣子:
另外添加一個(gè)main.c,在其中實(shí)現(xiàn)C語(yǔ)言的main函數(shù),并調(diào)用原python中的函數(shù):
extern void test_function();
int main() {
test_function();
return 0;
}
輸出結(jié)果:
可以正常工作!
2.轉(zhuǎn)換后的C代碼如何包裝成JNI接口使用
實(shí)際動(dòng)手
1.Python源代碼
def logic(param):
print('this is a logic function')
# 接口函數(shù),導(dǎo)出給Java Native的接口
def JNI_API_TestFunction(param):
print("enter JNI_API_test_function")
logic(param)
print("leave JNI_API_test_function")
2.使用Cython工具轉(zhuǎn)換成C代碼
3.編譯生成動(dòng)態(tài)庫(kù)
4.封裝為Jar包
準(zhǔn)備一個(gè)JNI調(diào)用的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個(gè)native方法:
- initModule: 對(duì)應(yīng)C代碼中Java_JNITest_initModule(),主要完成Python初始化
- uninitModule: 對(duì)應(yīng)C代碼中Java_JNITest_uninitModule(),主要完成Python反初始化
- TestFunction: 對(duì)應(yīng)C代碼中的Java_JNITest_TestFunction(),為核心業(yè)務(wù)接口
接口聲明文件+二進(jìn)制動(dòng)態(tài)庫(kù)文件準(zhǔn)備就緒,開(kāi)始打包:
jar -cvf JNITest.jar ./JNITest
5.Java調(diào)用
關(guān)鍵問(wèn)題
1.import問(wèn)題
上面演示的案例只是一個(gè)單獨(dú)的py文件,而實(shí)際工作中,我們的項(xiàng)目通常是具有多個(gè)py文件,并且這些文件通常是構(gòu)成了復(fù)雜的目錄層級(jí),互相之間各種import關(guān)系,錯(cuò)綜復(fù)雜。
Cython這個(gè)工具有一個(gè)最大的坑在于:經(jīng)過(guò)其處理的文件代碼中會(huì)丟失代碼文件的目錄層級(jí)信息,如下圖所示,C.py轉(zhuǎn)換后的代碼和m/C.py生成的代碼沒(méi)有任何區(qū)別。

這就帶來(lái)一個(gè)非常大的問(wèn)題:A.py或B.py代碼中如果有引用m目錄下的C.py模塊,目錄信息的丟失將導(dǎo)致二者在執(zhí)行import m.C時(shí)報(bào)錯(cuò),找不到對(duì)應(yīng)的模塊!
幸運(yùn)的是,經(jīng)過(guò)實(shí)驗(yàn)表明,在上面的圖中,如果A、B、C三個(gè)模塊處于同一級(jí)目錄下時(shí),import能夠正確執(zhí)行。
軒轅君曾經(jīng)嘗試閱讀Cython的源代碼,并進(jìn)行修改,將目錄信息進(jìn)行保留,使得生成后的C代碼仍然能夠正常import,但限于時(shí)間倉(cāng)促,對(duì)Python解釋器機(jī)理了解不足,在一番嘗試之后選擇了放棄。
在這個(gè)問(wèn)題上卡了很久,最終選擇了一種笨辦法:將樹(shù)形的代碼層級(jí)目錄展開(kāi)成為平坦的目錄結(jié)構(gòu),就上圖中的例子而言,展開(kāi)后的目錄結(jié)構(gòu)變成了
A.py
B.py
m_C.py
單是這樣還不夠,還需要對(duì)A、B中引用到C的地方全部進(jìn)行修正為對(duì)m_C的引用。
這看起來(lái)很簡(jiǎn)單,但實(shí)際情況遠(yuǎn)比這復(fù)雜,在Python中,import可不只有import這么簡(jiǎn)單,有各種各樣復(fù)雜的形式:
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 ...
除此之外,在代碼中還可能存在直接通過(guò)模塊進(jìn)行引用的寫(xiě)法。
展開(kāi)成為平坦結(jié)構(gòu)的代價(jià)就是要處理上面所有的情況!軒轅君無(wú)奈之下只有出此下策,如果各位大佬有更好的解決方案還望不吝賜教。
2.Python GIL問(wèn)題
Python轉(zhuǎn)換后的jar包開(kāi)始用于實(shí)際生產(chǎn)中了,但隨后發(fā)現(xiàn)了一個(gè)問(wèn)題:
每當(dāng)Java并發(fā)數(shù)一上去之后,JVM總是不定時(shí)出現(xiàn)Crash
隨后分析崩潰信息發(fā)現(xiàn),崩潰的地方正是在Native代碼中的Python轉(zhuǎn)換后的代碼中。
- 難道是Cython的bug?
- 轉(zhuǎn)換后的代碼有坑?
- 還是說(shuō)上面的import修正工作有問(wèn)題?

崩潰的烏云籠罩在頭上許久,冷靜下來(lái)思考:
為什么測(cè)試的時(shí)候正常沒(méi)有發(fā)現(xiàn)問(wèn)題,上線(xiàn)之后才會(huì)崩潰?
再次翻看崩潰日志,發(fā)現(xiàn)在native代碼中,發(fā)生異常的地方總是在malloc分配內(nèi)存的地方,難不成內(nèi)存被破壞了?
又敏銳的發(fā)現(xiàn)測(cè)試的時(shí)候只是完成了功能性測(cè)試,并沒(méi)有進(jìn)行并發(fā)壓力測(cè)試,而發(fā)生崩潰的場(chǎng)景總是在多并發(fā)環(huán)境中。多線(xiàn)程訪(fǎng)問(wèn)JNI接口,那Native代碼將在多個(gè)線(xiàn)程上下文中執(zhí)行。
猛地一個(gè)警覺(jué):99%跟Python的GIL鎖有關(guān)系!

眾所周知,限于歷史原因,Python誕生于上世紀(jì)九十年代,彼時(shí)多線(xiàn)程的概念還遠(yuǎn)遠(yuǎn)沒(méi)有像今天這樣深入人心過(guò),Python作為這個(gè)時(shí)代的產(chǎn)物一誕生就是一個(gè)單線(xiàn)程的產(chǎn)品。
雖然Python也有多線(xiàn)程庫(kù),允許創(chuàng)建多個(gè)線(xiàn)程,但由于C語(yǔ)言版本的解釋器在內(nèi)存管理上并非線(xiàn)程安全,所以在解釋器內(nèi)部有一個(gè)非常重要的鎖在制約著Python的多線(xiàn)程,所以所謂多線(xiàn)程實(shí)際上也只是大家輪流來(lái)占坑。
原來(lái)GIL是由解釋器在進(jìn)行調(diào)度管理,如今被轉(zhuǎn)成了C代碼后,誰(shuí)來(lái)負(fù)責(zé)管理多線(xiàn)程的安全呢?
由于Python提供了一套供C語(yǔ)言調(diào)用的接口,允許在C程序中執(zhí)行Python腳本,于是翻看這套API的文檔,看看能否找到答案。
幸運(yùn)的是,還真被我找到了:
獲取GIL鎖:

釋放GIL鎖:

在JNI調(diào)用入口需要獲得GIL鎖,接口退出時(shí)需要釋放GIL鎖。
加入GIL鎖的控制后,煩人的Crash問(wèn)題終于得以解決!
測(cè)試效果
準(zhǔn)備兩份一模一樣的py文件,同樣的一個(gè)算法函數(shù),一個(gè)通過(guò)Flask Web接口訪(fǎng)問(wèn),(Web服務(wù)部署于本地127.0.0.1,盡可能減少網(wǎng)絡(luò)延時(shí)),另一個(gè)通過(guò)上述過(guò)程轉(zhuǎn)換成Jar包。
在Java服務(wù)中,分別調(diào)用兩個(gè)接口100次,整個(gè)測(cè)試工作進(jìn)行10次,統(tǒng)計(jì)執(zhí)行耗時(shí):

上述測(cè)試中,為進(jìn)一步區(qū)分網(wǎng)絡(luò)帶來(lái)的延遲和代碼執(zhí)行本身的延遲,在算法函數(shù)的入口和出口做了計(jì)時(shí),在Java執(zhí)行接口調(diào)用前和獲得結(jié)果的地方也做了計(jì)時(shí),這樣可以計(jì)算出算法執(zhí)行本身的時(shí)間在整個(gè)接口調(diào)用過(guò)程中的占比。
- 從結(jié)果可以看出,通過(guò)Web API執(zhí)行的接口訪(fǎng)問(wèn),算法本身執(zhí)行的時(shí)間只占到了30%+,大部分的時(shí)間用在了網(wǎng)絡(luò)開(kāi)銷(xiāo)(數(shù)據(jù)包的收發(fā)、Flask框架的調(diào)度處理等等)。
- 而通過(guò)JNI接口本地調(diào)用,算法的執(zhí)行時(shí)間占到了整個(gè)接口執(zhí)行時(shí)間的80%以上,而Java JNI的接口轉(zhuǎn)換過(guò)程只占用10%+的時(shí)間,有效提升了效率,減少額外時(shí)間的浪費(fèi)。
- 除此之外,單看算法本身的執(zhí)行部分,同一份代碼,轉(zhuǎn)換成Native代碼后的執(zhí)行時(shí)間在300~500μs,而CPython解釋執(zhí)行的時(shí)間則在2000~4000μs,同樣也是相差懸殊。
總結(jié)
本文提供了一種Java調(diào)用Python功能的新思路,僅供參考,其成熟度和穩(wěn)定性還有待商榷,通過(guò)HTTP Restful接口訪(fǎng)問(wèn)仍然是跨語(yǔ)言對(duì)接的首選。
到此這篇關(guān)于Python代碼一鍵轉(zhuǎn)Jar包及Java調(diào)用Python新姿勢(shì)的文章就介紹到這了,更多相關(guān)Python轉(zhuǎn)Jar包內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用python實(shí)現(xiàn)簡(jiǎn)單去水印功能
這篇文章主要為大家詳細(xì)介紹了使用python實(shí)現(xiàn)簡(jiǎn)單去水印功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
詳解python的sorted函數(shù)對(duì)字典按key排序和按value排序
這篇文章主要介紹了詳解python的sorted函數(shù)對(duì)字典按key排序和按value排序,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
python數(shù)據(jù)結(jié)構(gòu)的排序算法
下面是是對(duì)python數(shù)據(jù)結(jié)構(gòu)的排序算法的一些講解及示意圖,感興趣的小伙伴一起來(lái)學(xué)習(xí)吧2021-08-08
在python中對(duì)變量判斷是否為None的三種方法總結(jié)
今天小編就為大家分享一篇在python中對(duì)變量判斷是否為None的三種方法總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
python數(shù)據(jù)寫(xiě)入列表并導(dǎo)出折線(xiàn)圖
這篇文章主要介紹了python數(shù)據(jù)寫(xiě)入列表并導(dǎo)出折線(xiàn)圖,文章以舉例展開(kāi)對(duì)文章主題的介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-01-01
學(xué)習(xí)createTrackbar的使用方法及步驟
這篇文章主要為大家介紹了學(xué)習(xí)createTrackbar的使用方法及步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-10-10

