CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: py4j で Java の API を Python から使う

今回は py4j を使って Java の API を Python から利用してみる。

py4j のアーキテクチャはサーバ・クライアントモデルになっている。 つまり、まず Java の API を Python から叩けるように、Java でゲートウェイサーバとなるプログラムを書く。 そして、Python からはネットワーク経由でそのゲートウェイサーバにアクセスする。 これは、RPC (Remote Procedure Call) の考え方に近い。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V
Python 3.7.3
$ java -version 
openjdk version "12.0.1" 2019-04-16
OpenJDK Runtime Environment (build 12.0.1+12)
OpenJDK 64-Bit Server VM (build 12.0.1+12, mixed mode, sharing)

下準備

下準備として Java (JDK) と py4j をインストールしておく。

$ brew cask install java
$ pip install py4j

ゲートウェイサーバを記述する

最初に、Python から利用したい Java の API に対してゲートウェイサーバを書く。

以下のサンプルコードでは HelloWorld クラスの API についてゲートウェイサーバを用意している。 基本的にはクラスのインスタンスを GatewayServer クラスに渡してやるだけ。 提供されるのは割り算の機能を持った div() メソッドになる。

import py4j.GatewayServer;

public class HelloWorld {

  public int div(int a, int b) {
    // 割り算の機能を提供するメソッド
    return a / b;
  }

  public static void main(String[] args) {
    // GatewayServer 経由で機能を提供する
    HelloWorld application = new HelloWorld();
    GatewayServer gateway = new GatewayServer(application);
    gateway.start();
    System.out.println("Starting server...");
  }
}

続いて、上記を Java バイトコードにコンパイルする。 ただし、それには py4j の jar ファイルが必要になる。

jar ファイルは py4j のインストール先にある。 なので、まずは py4j のインストールされている場所を確認する。

$ python -c "import py4j; print(py4j.__path__)"
['/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages/py4j']

次のように share ディレクトリ以下に jar ファイルがあった。

$ ls ~/.virtualenvs/py37/share/py4j
py4j0.10.8.1.jar

この jar ファイルにクラスパスを通しながら、先ほどのプログラムをコンパイルする。

$ javac -cp ~/.virtualenvs/py37/share/py4j/py4j0.10.8.1.jar HelloWorld.java

次のように class ファイルが完成すれば上手くいっている。

$ file HelloWorld.class 
HelloWorld.class: compiled Java class data, version 56.0

コンパイルできたら、ゲートウェイサーバのプログラムを起動する。 この際にも、jar ファイルにクラスパスを通す必要がある。

$ java -cp ~/.virtualenvs/py37/share/py4j/py4j0.10.8.1.jar:. HelloWorld
Starting server...

これで、デフォルトでは TCP:25333 で py4j のサービスが起動する。

$ lsof -i | grep 25333
java      10364 amedama    6u  IPv6 0xbd776a50dc8d3c71      0t0  TCP localhost:25333 (LISTEN)

Python から利用する

これで準備ができたらので、Python から利用してみよう。

まずは Python のインタプリタを起動する。

$ python

py4j 経由で Java の API を呼び出すために JavaGateway クラスのインスタンスを用意する。

>>> from py4j.java_gateway import JavaGateway
>>> java_gateway = JavaGateway()
>>> java_app = java_gateway.entry_point

あとは Java の API を呼び出すだけ。

>>> java_app.div(20, 10)
2

ちゃんと割り算ができている。

試しに Java のプログラム上で例外を発生させてみよう。 すると、次のように py4j.protocol.Py4JJavaError 例外となる。 例外の中には、Java 上で発生した例外の情報が入っている。

>>> java_app.div(1, 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages/py4j/java_gateway.py", line 1286, in __call__
    answer, self.gateway_client, self.target_id, self.name)
  File "/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages/py4j/protocol.py", line 328, in get_return_value
    format(target_id, ".", name), value)
py4j.protocol.Py4JJavaError: An error occurred while calling t.div.
: java.lang.ArithmeticException: / by zero
    at HelloWorld.div(HelloWorld.java:7)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:567)
    at py4j.reflection.MethodInvoker.invoke(MethodInvoker.java:244)
    at py4j.reflection.ReflectionEngine.invoke(ReflectionEngine.java:357)
    at py4j.Gateway.invoke(Gateway.java:282)
    at py4j.commands.AbstractCommand.invokeMethod(AbstractCommand.java:132)
    at py4j.commands.CallCommand.execute(CallCommand.java:79)
    at py4j.GatewayConnection.run(GatewayConnection.java:238)
    at java.base/java.lang.Thread.run(Thread.java:835)

いじょう。