在Java中更好的調用Python

原文鏈接:https://www.cnblogs.com/nuccch/p/8435693.html

寫在前面

在微服務架構大行其道的今天,對於將程序進行嵌套調用的做法其實並不可取,甚至顯得有些愚蠢。當然,之所以要面對這個問題,或許是因爲一些歷史原因,或者僅僅是爲了簡單。恰好我在項目中就遇到了這個問題,需要在Java程序中調用Python程序。關於在Java中調用Python程序的實現,根據不同的用途可以使用多種不同的方法,在這裏就將在Java中調用Python程序的方式做一個總結。

直接通過Runtime進行調用

我們知道,在Java中如果需要調用第三方程序,可以直接通過Runtime實現,這也是最直接最粗暴的做法。

public class InvokeByRuntime {
    /**
     * @param args
     * @throws IOException 
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws IOException, InterruptedException {
        String exe = "python";
        String command = "D:\\calculator_simple.py";
        String num1 = "1";
        String num2 = "2";
        String[] cmdArr = new String[] {exe, command, num1, num2};
        Process process = Runtime.getRuntime().exec(cmdArr);
        InputStream is = process.getInputStream();
        DataInputStream dis = new DataInputStream(is);
        String str = dis.readLine();
        process.waitFor();
        System.out.println(str);
    }
}

輸出:

3

calculator_simple.py:

# coding=utf-8
from sys import argv

num1 = argv[1]
num2 = argv[2]
sum = int(num1) + int(num2)
print sum

顯然,在Java中通過Runtime調用Python程序與直接執行Python程序的效果是一樣的,可以在Python中讀取傳遞的參數,也可以在Java中讀取到Python的執行結果。需要注意的是,不能在Python中通過return語句返回結果,只能將返回值寫入到標準輸出流中,然後在Java中通過標準輸入流讀取Python的輸出值。

通過Jython調用

通過Jython調用Python?我在聽到這個概念的時候一臉懵逼,不是說好的在Java中調用Python程序嗎?這個Jython是什麼鬼?難道是一個在Java中調用Python程序的組件或工具?其實,關於Jython是什麼這個疑問,我估計有許多人在一開始接觸的時候也是很疑惑的,下面我們就一一道來。

1. 什麼是Jython

Jython主頁:https://www.jython.org/
按照官方的定義,Jython是Python語言在Java平臺的實現。這個概念似乎有點拗口,反正我一開始並沒有理解。Python難道不已經是一門語言了嗎?什麼叫做Jython是Python語言在Java平臺的實現?
實際上,之所以存在這樣的困惑主要是因爲我們對Python語言的相關概念掌握和理解不清楚導致的。
Python其實只是一個語言規範,它存在多個不同語言實現的版本。具體來說,目前Python語言存在如下幾個具體實現:
(1)CPython:CPython是標準Python,也是其他Python編譯器的參考實現。通常提到“Python”一詞,都是指CPython。CPython由C編寫,將Python源碼編譯成CPython字節碼,由虛擬機解釋執行。沒有用到JIT等技術,垃圾回收方面採用的是引用計數。
(2)Jython:Jython是在JVM上實現的Python,由Java編寫。Jython將Python源碼編譯成JVM字節碼,由JVM執行對應的字節碼。因此能很好的與JVM集成,比如利用JVM的垃圾回收和JIT,直接導入並調用JVM上其他語言編寫的庫和函數。
(3)IronPython:IronPython與Jython類似,所不同的是IronPython在CLR上實現的Python,即面向.NET平臺,由C#編寫。IronPython將源碼編譯成TODO CLR,同樣能很好的與.NET平臺集成。即與Jython相同,可以利用.NET框架的JIT、垃圾回收等功能,能導入並調用.NET上其他語言編寫的庫和函數。IronPython默認使用Unicode字符串。
(4)PyPy:這裏說的PyPy是指使用RPython實現,利用Tracing JIT技術實現的Python,而不是RPython工具鏈。PyPy可以選擇多種垃圾回收方式,如標記清除、標記壓縮、分代等。
(5)Pyston:Pyston由Dropbox開發,使用C++11編寫,採用Method-at-a-time-JIT和Mark Sweep——Stop the World的GC技術。Pyston使用類似JavaScript V8那樣的多層編譯,其中也用到了LLVM來優化代碼。

所以,我們現在再來理解什麼是Jython就非常清楚了:Jython是Python語言規範在Java平臺的具體實現。具體來說,可以將Python源碼編譯爲JVM可以解釋執行的字節碼。
Jython原本叫做JPython,於1997年由Jim Hugunin創建,後來在1999年2.0版本發佈的時候由Barry Warsaw更名爲Jython,在這裏我們就不再深究爲什麼要把JPython更名爲Jython的原因了。注意: Jython從2.0版本開始就與CPython的版本保持一致,即:Jython 2.7與CPython 2.7保持對應。

雖然我們理解了什麼是Jython,但是還存在一個疑問,爲什麼Python語言存在這麼多不同語言的實現呢?爲什麼不能就只存在一個C語言實現的版本就可以了呢?存在這麼多版本,真的給初學者帶來了許多困惑。
當然,要回答這個問題可能就需要深究一些歷史的原因了,就此打住。我們在此只討論使用Jython能做什麼以及如何使用Jython?

2. 使用Jython能做什麼

既然Jython是Python語言在Java平臺的實現,是Java語言實現的,那麼是否可以在Jython程序中調用Java,在Java中也能調用Jython呢?
答案是肯定的,實際上,Jython的主要通途就是在Java中調用Python程序;而且,還可以直接在Jython程序中引用Java。

3. 如何使用Jython

3.1 安裝Jython

在Jython的官方下載頁面我們可以看到如下描述(詳見:http://www.jython.org)

顯然,可以下載2個Jython的jar包。其中,jython-installer-${version}.jar是用於安裝Jython的,jython-standalone-${version}.jar用於嵌入到Java程序中使用。
什麼意思?我一開始也是很疑惑,爲什麼要提供2個不同的jar包呢?他們有什麼不同呢?2個不同的Jar包如何使用呢?
首先,jython-installer-${version}.jar用於安裝Jython,就好比我們需要安裝JRE,用於運行Java程序。除此之外,當需要在Python程序中引用一些公共的第三方庫時,也需要先安裝Jython才能下載所依賴的模塊。

下載jython-installer-${version}.jar完畢之後,進入控制檯,執行如下命令:

java -jar jython-installer-${version}.jar

此時會彈出一個圖形化的安裝界面,只需要一步一步選擇相應參數進行安裝即可。安裝完畢之後,請將Jython安裝目錄添加爲環境變量JYTHON_HOME,同時添加bin目錄到PATH變量中:PATH=$PATH:$JYTHON_HOME/bin
進入控制檯,執行如下命令就可以進入Jython的交互環境,這與CPython(我們通常說的Python)的命令行交互環境是一樣的。

> jython
Jython 2.7.0 (default:9987c746f838, Apr 29 2015, 02:25:11)
[Java HotSpot(TM) 64-Bit Server VM (Oracle Corporation)] on java1.8.0_121
Type "help", "copyright", "credits" or "license" for more information.
>>> print("hello,world")
hello,world
>>>

當然,我們還可以使用jython命令運行一個Python程序。

> jython helloworld.py
hello,world

helloworld.py:

import sys

print("hello,world")

上面我們看到在Jython官網提供了2個Jar包,一個用於安裝Jython,執行Python程序。那麼,jython-standalone-${version}.jar又有什麼用途呢?
實際上,當我們需要在Java中調用Python程序時,除了直接使用Java的Runtime調用,還可以直接使用Jython的API進行調用,而且通過Jython API可以直接調用Python程序中的指定函數或者對象方法,粒度更加精細。
當我們需要調用Jython的API時有兩種方式:
第一,如果項目使用Maven進行構建,可以直接添加Jython的依賴配置到pom.xml文件中,如:

<dependency>
    <groupId>org.python</groupId>
    <artifactId>jython</artifactId>
    <version>2.7.0</version>
</dependency>

第二,可以直接將jython-standalone-${version}.jar添加到項目classpath中,這樣也可以調用Jython的相關API了。也就是說,jython-standalone-${version}.jar就是一個提供Jython API的jar獨立jar包。

3.2 Java調用Python程序實踐

Java通過Jython API調用Python程序,有幾種用法:
(1)在Java中執行Python語句,相當於在Java中嵌入了Python程序,這種用法不常見,也沒有太大的實際意義。

public static void main(String[] args) {
    System.setProperty("python.home", "D:\\jython2.7.0");
    PythonInterpreter interp = new PythonInterpreter();
    // 執行Python程序語句
    interp.exec("import sys");
    interp.set("a", new PyInteger(42));
    interp.exec("print a");
    interp.exec("x = 2+2");
    PyObject x = interp.get("x");
    System.out.println("x: " + x);
}

輸出:

42
x: 4

(2)在Java中簡單調用Python程序,不需要傳遞參數,也不需要獲取返回值。

public static void main(String[] args) throws IOException {
    System.setProperty("python.home", "D:\\jython2.7.0");
    String python = "D:\\simple_python.py";
    PythonInterpreter interp = new PythonInterpreter();
    interp.execfile(python);
    interp.cleanup();
    interp.close();
}

simple_python.py:

# coding=utf-8
print("Do simple thing in Python")
print("輸出中文")

(3)在Java中單向調用Python程序中的方法,需要傳遞參數,並接收返回值。Python既支持面向函數式編程,也支持面向對象編程。因此,調用Python程序中的方法也分別以面向函數式編程和麪向對象式編程進行說明。

public static void main(String[] args) throws IOException {
    System.setProperty("python.home", "D:\\jython2.7.0");
    
    // 1. Python面向函數式編程: 在Java中調用Python函數
    String pythonFunc = "D:\\calculator_func.py";
    
    PythonInterpreter pi1 = new PythonInterpreter();
    // 加載python程序
    pi1.execfile(pythonFunc);
    // 調用Python程序中的函數
    PyFunction pyf = pi1.get("power", PyFunction.class);
    PyObject dddRes = pyf.__call__(Py.newInteger(2), Py.newInteger(3));
    System.out.println(dddRes);
    pi1.cleanup();
    pi1.close();
    
    // 2. 面向對象式編程: 在Java中調用Python對象實例的方法
    String pythonClass = "D:\\calculator_clazz.py";
    // python對象名
    String pythonObjName = "cal";
    // python類名
    String pythonClazzName = "Calculator";
    PythonInterpreter pi2 = new PythonInterpreter();
    // 加載python程序
    pi2.execfile(pythonClass);
    // 實例化python對象
    pi2.exec(pythonObjName + "=" + pythonClazzName + "()");
    // 獲取實例化的python對象
    PyObject pyObj = pi2.get(pythonObjName);
    // 調用python對象方法,傳遞參數並接收返回值
    PyObject result = pyObj.invoke("power", new PyObject[] {Py.newInteger(2), Py.newInteger(3)}); 
    double power = Py.py2double(result);
    System.out.println(power);
    
    pi2.cleanup();
    pi2.close();
}

輸出:

8.0
8.0

calculator_func.py:

# coding=utf-8
import math

# 面向函數式編程
def power(x, y):
    return math.pow(x, y)

calculator_clazz.py:

# coding=utf-8
import math

# 面向對象編程
class Calculator(object):
    
    # 計算x的y次方
    def power(self, x, y):
        return math.pow(x,y)

(4)高級調用,也是在Java中調用Python程序最常見的用法:Python程序可以實現Java接口,在Python中也可以調用Java方法。

public static void main(String[] args) throws IOException {
    System.setProperty("python.home", "D:\\jython2.7.0");
    
    // Python程序路徑
    String python = "D:\\python\\fruit_controller.py";
    // Python實例對象名
    String pyObjName = "pyController";
    // Python類名
    String pyClazzName = "FruitController";
    
    Fruit apple = new Apple();
    Fruit orange = new Orange();
    
    PythonInterpreter interpreter = new PythonInterpreter();
    // 如果在Python程序中引用了第三方庫,需要將這些被引用的第三方庫所在路徑添加到系統環境變量中
    // 否則,在執行Python程序時將會報錯: ImportError: No module named xxx
    PySystemState sys = interpreter.getSystemState();
    sys.path.add("D:\\python");
    
    // 加載Python程序
    interpreter.execfile(python);
    // 實例 Python對象
    interpreter.exec(pyObjName + "=" + pyClazzName + "()");

    // 1.在Java中獲取Python對象,並將Python對象轉換爲Java對象
    // 爲什麼能夠轉換? 因爲Python類實現了Java接口,通過轉換後的Java對象只能調用接口中定義的方法
    GroovyController controller = (GroovyController) interpreter.get(pyObjName).__tojava__(GroovyController.class);
    controller.controllFruit(apple);
    controller.controllFruit(orange);
    
    // 2.在Java直接通過Python對象調用其方法
    // 既可以調用實現的Java接口方法,也可以調用Python類自定義的方法
    PyObject pyObject = interpreter.get(pyObjName);
    pyObject.invoke("controllFruit", Py.java2py(apple));
    pyObject.invoke("controllFruit", Py.java2py(orange));
    pyObject.invoke("printFruit", Py.java2py(apple));
    pyObject.invoke("printFruit", Py.java2py(orange));
    
    // 3.在Java中獲取Python類進行實例化對象: 沒有事先創建 Python對象
    PyObject pyClass = interpreter.get("FruitController");
    PyObject pyObj = pyClass.__call__();
    pyObj.invoke("controllFruit", Py.java2py(apple));
    pyObj.invoke("controllFruit", Py.java2py(orange));
    
    PyObject power = pyObj.invoke("power", new PyObject[] {Py.newInteger(2), Py.newInteger(3)});
    if(power != null) {
        double p = Py.py2double(power);
        System.out.println(p);
    }
    
    interpreter.cleanup();
    interpreter.close();
}

輸出:

Show: I am a java apple.
controllFruit Python Apple
controllFruit END
Show: I am a java orange.
controllFruit Python Orange
controllFruit END
Show: I am a java apple.
controllFruit Python Apple
controllFruit END
Show: I am a java orange.
controllFruit Python Orange
controllFruit END
Show: I am a java apple.
printFruit Python Apple
printFruit END
Show: I am a java orange.
printFruit Python Orange
printFruit END
Show: I am a java apple.
controllFruit Python Apple
controllFruit END
Show: I am a java orange.
controllFruit Python Orange
controllFruit END
8.0

fruit_controller.py:

# coding=utf-8

from calculator_clazz import Calculator
from java.lang import String
from org.test.inter import GroovyController
from org.test.inter import Fruit

# 在Python中實現Java接口: org.test.inter.GroovyController
class FruitController(GroovyController):
    
    # 實現接口方法
    def controllFruit(self, fruit):
        # 在Python中調用Java對象方法
        fruit.show()
        
        if(fruit.getType() == "apple"):
            print ("controllFruit Python Apple")
            
        if(fruit.getType() == "orange"):
            print ("controllFruit Python Orange")
        
        print ("controllFruit END")
    
    # 自定義新方法    
    def printFruit(self, fruit):
        fruit.show()
        
        if(fruit.getType() == "apple"):
            print ("printFruit Python Apple")
            
        if(fruit.getType() == "orange"):
            print ("printFruit Python Orange")
        
        print ("printFruit END")
    
    # 引用第三方python程序
    def power(self, x, y):
        cal = Calculator()
        return cal.power(x, y)

Java接口和實現類:

// 該接口用於在Python中實現
public interface GroovyController {
    public void controllFruit(Fruit fruit);
}

// 在Java中使用的接口
public interface Fruit {
    public String getName();
    public String getType();
    public void show();
}

// Apple
public class Apple implements Fruit {
    public String getName() {
        return "java apple";
    }
    
    public String getType() {
        return "apple";
    }

    public void show() {
        System.out.println("Show: I am a java apple.");
    }
}

// Orange
public class Orange implements Fruit {
    public String getName() {
        return "ava orange";
    }

    public String getType() {
        return "orange";
    }

    public void show() {
        System.out.println("Show: I am a java orange.");
    }
}

另外,對於在eclipse中運行時控制檯報錯:

Failed to install '': java.nio.charset.UnsupportedCharsetException: cp0

請添加VM參數:-Dpython.console.encoding=UTF-8,詳見:http://blog.csdn.net/xfei365/article/details/50955731

總結

雖然在Java中調用Python可以有多種方式解決,甚至因爲Jython的出現更顯得非常便利。但是這種程序間嵌套調用的方式不可取,首先拋開調用性能不說,增加了耦合複雜度。更加有效的方式應該是通過RCP或者RESTful接口進行解耦,這樣各司其職,也便於擴展,良好的架構是一個項目能夠健康發展的基礎。在微服務架構大行其道的今天,這種程序間嵌套調用的方式將會逐漸被淘汰。

【參考】
http://tonl.iteye.com/blog/1918245 Java調用Python
http://blog.csdn.net/supermig/article/details/24005585 Learning Python -- Java 通過JyThon調用Python實現的規則
http://blog.csdn.net/hpp1314520/article/details/72854011 java 利用Runtime.getRuntime().exec()調用python腳本並傳參
http://blog.csdn.net/xingjiarong/article/details/49424253 java調用python方法總結
https://zh.wikipedia.org/wiki/Jython Jython
http://lib.csdn.net/article/python/1654 Jython的安裝及簡單例子
https://coolshell.cn/articles/2631.html 五大基於JVM的腳本語言
http://python.jobbole.com/82703/ 各種 Python 實現的簡單介紹與比較
https://www.oschina.net/translate/why-are-there-so-many-pythons 爲什麼有這麼多 Python?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章