使用JEP(Java Embedded Python)库实现Java调用Python

4,197 阅读10分钟

简介

Jep是利用了JNI(Java Native Interface)来使得Java和Python能在同一个进程中直接通信。通过使用JNI以及CPython API,Jep得以启动JVM中的Python解释器。它可以与各种CPython扩展一起工作。开发人员报告说,Jep可以与NumPy、Scipy、Pandas、TensorFlow、Matplotlib、cvxpy等一起工作。

Jep 将自动转换 Java 原语、字符串和 jep.NDArrays并分别发送到Python解释器中,分别转换为Python原语,字符串和numpy.ndarrays。这些对象的 Python 版本不会引用它们的原始 Java 对应项,它们是仅存在于 Python 系统内存中的全新对象。

与上面列出的类型之一不匹配的 Java 对象将自动包装为 PyJObject(或其相关类之一)。PyJObject 包装对原始 Java 对象的引用,并为 Python 解释器提供一个接口,用于将对象理解为 Python 对象。从Python解释器的角度来看,PyJObject只是另一个Python对象,其上有一组选定的属性(字段和方法)。

Python 字符串、原语和 numpy.ndarrays 在传递/返回给 Java 时将自动转换为它们的 Java 等效项。这些 Java 对象将是等效的副本,而不是对 Python 对象的引用。 Jep支持一些自动转换:

Python 对象 -> Java 对象:

  • None -> null
  • PyJClass (wrapped class) -> java.lang.Class
  • PyJObject (wrapped object) -> java.lang.Object
  • Python 2 Str -> java.lang.String
  • Python 3 Str -> java.lang.String
  • Python 3 Unicode -> java.lang.String
  • True -> java.lang.Boolean
  • False -> java.lang.Boolean
  • Python 2 Int -> java.lang.Integer
  • Python 2 Long -> java.lang.Long
  • Python 3 Int -> java.lang.Long
  • Float -> java.lang.Double
  • List -> java.util.ArrayList
  • Tuple -> Collections.unmodifiableList(ArrayList)
  • Dict -> java.util.HashMap
  • Callable -> jep.python.PyCallable
  • numpy.ndarray -> jep.NDArray
  • object -> java.lang.String (This is a last resort where returns . Do not depend on this behavior, it will change in the future).Interpreter.getValue(String)``str(pyobject)

Jep的好处:

  • Java和Python完全在一个进程中,所需用的资源应该最少
  • Java和Python的调用就像是函数调用,执行代价很低

安装

要使用jep实现Java调用Python,您需要先在Python中安装jep库,然后在IDEA中导入jep包,并创建一个Jep对象来执行Python代码或调用Python函数。

1)配置Python所需环境:(构建和安装需要预先安装 JDK、Python 和可选的 numpy。 )

  • Python >= 3.5
  • Java >= 1.8
  • NumPy >= 1.7 (optional)
  • 操作系统:本文使用Windows10

2)idea引入JEP:

方法一:引入jep的jar包,下载地址为:repo1.maven.org/maven2/blac…

方法二:添加Maven依赖:

<!-- https://mvnrepository.com/artifact/black.ninia/jep --><dependency>
    <groupId>black.ninia</groupId>
    <artifactId>jep</artifactId>
    <version>4.1.1</version>
</dependency>

应用案例

本文的应用案例可简单描述为:在由Java编写的算法程序中,需要调用Python的Fuzzy Extractor库,以实现密钥生成与再生功能。

以下为所需调用的两个Python方法的代码:

1)fuzzyGen.py:

from fuzzy_extractor import FuzzyExtractor
//密钥生成方法
//传入参数为字符串类型的rid
//输出为密钥key以及辅助数据helper
def Gen(Rid):
    extractor = FuzzyExtractor(16, 8)
    key, helper = extractor.generate(Rid)
    return key, helper

2)fuzzyRep.py:

from fuzzy_extractor import FuzzyExtractor
//密钥再生方法,通过传入辅助数据helper以及带噪音的字符pid,恢复密钥key
//该算法的主要思想是:只要带噪音的字符串pid与原始字符rid之间足够相似(在这里,两字符间可容忍的最大汉明距离为8),就能精确恢复出密钥key
def Rep(Pid,helper):
    extractor = FuzzyExtractor(16, 8)
    r_key = extractor.reproduce(Pid, helper)
    return r_key

JEP的核心思想为在JVM中启动Python解释器,在Python解释器中执行所需Python语句,并通过该解释器完成Java与Python之间的变量传递。

使用JEP执行Python代码的方式十分简单,首先需要配置共享库jep.dll及jep python代码的路径(注意jep.dll文件在pip install jep时已顺带生成,因此在python库文件夹中)。然后,便可创建Interpreter实例,调用相关python方法。

注意:当你在Java中创建一个Interpreter实例时,将为该Java Interpreter实例创建一个Python解释器,并保留在内存中,直到用Interpreter.close()关闭该Interpreter实例。由于需要管理一致的Python线程状态,创建Interpreter实例的线程必须在对该Interpreter实例的所有方法调用中重复使用。

以下为代码示例:

String rid = "AABBCCDDEEFFGGHH";
String pid = "AABBCCKDEEMFGGHI";
​
MainInterpreter.setJepLibraryPath("C:\Users\mattlennon\AppData\Local\Programs\Python\Python36\Lib\site-packages\jep\jep.dll");
JepConfig config = new JepConfig();
config.addIncludePaths("D:\jepTest");
 try (Interpreter interp = new SubInterpreter(config)){
     //使用eval方法,在python中引入相关依赖   
     interp.eval("from fuzzyGen import *");
     interp.eval("from fuzzyRep import *");
     //使用set方法,将变量rid的数值从java传递到Python中
     interp.set("rid", rid);
     //使用eval方法,执行python语句“A, B = Gen(rid)”,调用模糊提取器的密钥生成算法
     interp.eval("A, B = Gen(rid)");
     //从interpreter中获取返回结果
     jep.python.PyObject A = interp.getValue("A", jep.python.PyObject.class);
     jep.python.PyObject B = interp.getValue("B", jep.python.PyObject.class);
     //使用set方法,将变量pid的数值从java传递到Python中
     interp.set("pid", pid);
     //使用eval方法,执行python语句“r_A = Rep(pid, B)”,调用模糊提取器的密钥生成算法
     interp.eval("r_A = Rep(pid, B)");
     //从interpreter中获取返回结果
     jep.python.PyObject r_A = interp.getValue("r_A", jep.python.PyObject.PyObject.class);     
     System.out.println("reproduce key success:" + r_A.equals(A));
     //reproduce key success:true
 }

JEP库解析

JEP库主要由以下几个模块组成:

  • jep包:提供了Jep类和相关的接口和异常类,是使用jep库的主要入口点。
  • jep.python包:提供了PyCallable、PyConfig、PyModule等类,用于表示和操作Python对象。
  • jep.util包:提供了一些工具类,如MemoryManager、ReflectionUtils等,用于管理内存和反射操作。
  • jep.console包:提供了Console类和相关的接口和异常类,用于实现一个交互式的Python控制台。
  • jep.scripting包:提供了ScriptEvaluator、ScriptRunner等类,用于执行Python脚本文件或字符串。

下面我们来详细介绍一下这些模块及其包含的类与方法,并给出一些使用示例。

jep包

jep包是使用jep库的主要入口点,它提供了Jep类和相关的接口和异常类。Jep类是一个实现了AutoCloseable接口的类,它代表了一个嵌入在Java中的Python解释器实例。Jep类有以下一些主要的方法:

  • Jep():构造一个默认的Jep实例,使用当前线程的类加载器和系统属性。
  • Jep(PyConfig config):构造一个带有指定配置参数的Jep实例。
  • Jep(ClassLoader classLoader):构造一个使用指定类加载器的Jep实例。
  • Jep(ClassLoader classLoader, PyConfig config):构造一个使用指定类加载器和配置参数的Jep实例。
  • close():关闭这个Jep实例,并释放相关资源。这个方法会自动被try-with-resources语句调用。
  • eval(String script):执行一段Python代码,并返回最后一个表达式的值,如果没有表达式,则返回null。
  • exec(String script):执行一段Python代码,但不返回任何值。
  • exec(Path path):执行一个Python脚本文件,但不返回任何值。
  • exec(File file):执行一个Python脚本文件,但不返回任何值。
  • getValue(String name):获取Python中指定名称的对象,并转换为Java对象。如果不存在该名称,则抛出异常。
  • setValue(String name, Object value):将Java对象赋值给Python中指定名称的对象。如果不存在该名称,则创建一个新对象。
  • getAttribute(Object target, String name):获取Python对象中指定属性的值,并转换为Java对象。如果不存在该属性,则抛出异常。
  • setAttribute(Object target, String name, Object value):将Java对象赋值给Python对象中指定属性的值。如果不存在该属性,则创建一个新属性。

jep.python包

jep.python包是一个提供了一些Python相关的工具类和接口的包,它主要用于在Java中调用Python代码或对象时进行类型转换、异常处理、引用计数等操作。jep.python包中有以下一些主要的类和接口:

  • PyCallable:一个表示Python可调用对象(如函数、方法、类等)的接口,它继承了AutoCloseable接口,可以通过close()方法释放对Python对象的引用。
  • PyConfig:一个表示Python解释器配置参数的类,它可以设置Python路径、交互模式、输出重定向等选项。
  • PyModule:一个表示Python模块对象的类,它继承了PyCallable接口,可以通过get(String name)方法获取模块中的属性或对象。
  • PyObject:一个表示任意Python对象的类,它继承了AutoCloseable接口,可以通过close()方法释放对Python对象的引用。它还提供了一些静态方法和实例方法来创建或转换不同类型的Python对象。
  • PyPointer:一个表示指向Python内存地址的指针对象的类,它继承了AutoCloseable接口,可以通过close()方法释放对Python内存地址的引用。它主要用于在Java层和JNI层之间传递数据。
  • PythonException:一个表示从Python抛出到Java层的异常对象的类,它继承了RuntimeException类,并封装了原始的Python异常信息。

jep.util包

jep.util包是一个提供了一些工具类和方法的包,它主要用于在Java中处理Python对象或数据时进行一些辅助操作。jep.util包中有以下一些主要的类和方法:

  • IdentityHashMap:一个实现了Map接口的类,它使用对象的身份(即内存地址)作为键,而不是对象的equals()方法。这个类主要用于在Java层缓存Python对象,以避免重复创建或释放引用。
  • MemoryManager:一个管理Python内存分配和释放的类,它提供了一些静态方法来获取或释放Python内存地址对应的PyPointer对象。这个类主要用于在JNI层和Python层之间传递数据。
  • SharedInterpreter:一个继承了Jep类的子类,它可以创建一个共享同一个Python解释器实例的Jep对象。这个类主要用于在多线程环境中使用Jep,以避免每个线程创建一个独立的解释器实例。
  • StringUtil:一个提供了一些字符串相关的工具方法的类,它提供了一些静态方法来转换或比较不同编码格式的字符串。这个类主要用于在Java层和JNI层之间处理字符串数据。

代码示例:

// 使用IdentityHashMap缓存Python对象 
IdentityHashMap<Object, PyObject> cache = new IdentityHashMap<>(); 
try (Jep jep = new Jep()) 
{ // 获取Python中的一个列表对象 
    PyObject pyList = jep.getValue(“[1, 2, 3]”); 
    // 将列表对象和其对应的PyObject对象放入缓存中 
    cache.put(pyList.getObject(), pyList); 
    // 获取Python中的另一个列表对象,它与前一个列表对象相等,但不是同一个对象 
    PyObject anotherPyList = jep.getValue(“[1, 2, 3]”); 
    // 检查缓存中是否已经存在该列表对象对应的PyObject对象 
    if (cache.containsKey(anotherPyList.getObject())) { 
        System.out.println(“Found in cache”); 
    } 
    else { 
        System.out.println(“Not found in cache”); 
        // 将该列表对象和其对应的PyObject对象放入缓存中 
        cache.put(anotherPyList.getObject(), anotherPyList); 
    } 
}
​
// 使用MemoryManager获取或释放Python内存地址 
try (Jep jep = new Jep()) { 
    // 获取Python中的一个整数对象 
    PyObject pyInt = jep.getValue(“42”); 
    // 获取该整数对象对应的内存地址 
    long address = pyInt.getAddress(); 
    // 通过内存地址获取一个PyPointer对象,该对象指向Python内存空间中的数据 
    PyPointer pointer = MemoryManager.getPyObject(address); 
    // 通过PyPointer对象获取其指向的数据,并转换为Java整数 
    int value = pointer.asInt(); 
    System.out.println(value); 
    // 释放PyPointer对象,减少对Python内存空间中数据的引用计数 
    MemoryManager.release(pointer); 
}
​
// 使用SharedInterpreter创建共享同一个解释器实例的Jep对象 
// 创建一个SharedInterpreter实例,它会初始化一个全局的解释器实例,并将当前线程绑定到该实例上 
try (SharedInterpreter si = new SharedInterpreter()) { 
    // 执行一段Python代码,在全局解释器实例中定义一个变量x并赋值为10 
    si.exec(“x = 10”); 
    // 在另一个线程中创建另一个SharedInterpreter实例,它会使用已经存在的全局解释器实例,并将当前线程绑定到该实例上 
    new Thread(() -> { try (SharedInterpreter si2 = new SharedInterpreter()) { 
        // 在另一个线程中执行一段Python代码,在全局解释器实例中获取变量x并打印其值 
        si2.exec(“print(x)”); 
    } catch (JepException e) { 
        e.printStackTrace(); 
    } }).start(); 
}
​
// 使用StringUtil转换或比较不同编码格式的字符串 
try (Jep jep = new Jep()) { 
    // 获取Python中的一个UTF-8编码格式的字符串 
    PyObject pyStr = jep.getValue(“‘你好’”); 
    // 将PyObject转换为Java字节数组,并指定编码格式为UTF-8 
    byte[] bytes = StringUtil.toBytes(pyStr, “UTF-8”); 
    System.out.println(Arrays.toString(bytes)); 
    // 将Java字节数组转换为PyObject,并指定编码格式为UTF-8 
    PyObject anotherPyStr = StringUtil.toPyObject(bytes, “UTF-8”); 
    System.out.println(anotherPyStr); 
    // 比较两个PyObject是否相等,忽略编码格式差异 
    boolean equal = StringUtil.equals(pyStr, anotherPyStr); System.out.println(equal); 
}

jep.console包

jep.console包是一个提供了一些控制台相关的工具类和方法的包,它主要用于在Java中创建或使用Python交互式控制台时进行一些输入输出操作。jep.console包中有以下一些主要的类和方法:

  • Console:一个表示Python交互式控制台的接口,它定义了一些抽象方法来获取用户输入、输出Python结果、处理Python异常等。
  • ConsoleFactory:一个创建Console实例的工厂类,它提供了一些静态方法来根据不同的参数或环境创建不同类型的Console实例。
  • InteractiveConsole:一个实现了Console接口的类,它使用JLine库来提供一个带有自动补全、历史记录等功能的Python交互式控制台。
  • JLineConsole:一个继承了InteractiveConsole类的子类,它使用JLine3库来提供一个更加强大和灵活的Python交互式控制台。
  • Main:一个包含main()方法的类,它可以用于启动一个独立的Python交互式控制台程序。

代码示例:

// 使用ConsoleFactory创建一个默认的Console实例 
try (Jep jep = new Jep()) { 
    // 创建一个默认的Console实例,它会根据当前环境选择合适的控制台类型 
    Console console = ConsoleFactory.create(jep); 
    // 启动控制台,进入Python交互模式 
    console.interact(); 
}
​
// 使用InteractiveConsole创建一个带有自动补全功能的Console实例 
try (Jep jep = new Jep()) { 
    // 创建一个InteractiveConsole实例,指定使用Tab键作为自动补全触发键 
    InteractiveConsole console = new InteractiveConsole(jep, ‘\t’); 
    // 启动控制台,进入Python交互模式 
    console.interact(); 
}
​
// 使用JLineConsole创建一个带有多种功能的Console实例 
try (Jep jep = new Jep()) { 
    // 创建一个JLineConsole实例,指定使用Ctrl+Space键作为自动补全触发键,并开启多行输入模式和语法高亮模式 
    JLineConsole console = new JLineConsole(jep, KeyMap.ctrl(’ '), true, true); 
    // 启动控制台,进入Python交互模式 
    console.interact(); 
}
​
// 使用Main类启动一个独立的Python交互式控制台程序 
public class MainDemo { 
    public static void main(String[] args) throws Exception { 
        // 调用Main类的main()方法,传入命令行参数数组 
        Main.main(args); 
    } 
}