简介
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);
}
}