PJRmi,Python和Java之间的RMI
项目描述
PJRmi
概述
PJRmi 是一个API,用于从Python进程中在Java进程中执行远程方法调用(RMI,也称为RPC)。它的工作原理是在Python中创建shim对象,用户会将其透明地视为大多数Python对象,但实际上会在Java进程中引起事件。
PJRmi通过反射执行所有操作,因此作为服务提供者,您只需要实现它的getObjectInstance()
方法(见下文)。
除了下面的示例外,您还可以尝试Jupyter notebook的实时版本。
亮点
PJRmi的一些主要功能包括
- Java和Python类型之间的无缝互操作。
- 双向调用;Python到Java,或Java到Python。
- 支持Java接口的lambda和鸭子类型Python实现。
- 灵活可扩展的连接选项,从进程内到基于网络的。
- 线程安全执行,具有内置锁定支持和通过future的异步执行。
- 实时代码注入。
- 一个类似numpy的Java数学库,它也可以直接与Python互操作。
用例示例
- Java应用的脚本化。
- 将Java服务接口暴露给Python客户端。
- 命令和控制。
- 即时调试和/或dev-ops。
其他工作
PJRmi并不是唯一实现Python和Java之间桥梁的工具;还有一些其他实现:
- JEP通过JNI将CPython嵌入到Java中,允许Java调用Python。这是一个纯进程内操作。
- py4j允许Python调用Java。它还支持Java回调到Python,以便Python客户端可以实现Java接口。它是通过套接字通信来工作的。
- jpy是另一个进程内实现。其一个关键特性是支持通过指针传递使用数组的快速按值传递操作。
- jpype是另一个进程内实现。因为它也使用基于内部的C语言传递,所以性能非常高。
除了上述功能集,PJRmi还支持复杂的Java结构,两种语言的类型系统集成流畅,并且可以在不同的操作模式下透明地使用。
安装
如果您想尝试PJRmi,有以下几种选择
pip安装pjrmi
- 克隆此存储库,并在其中运行
./gradlew wheel
,然后使用pip install
安装。
请注意,PyPI上的版本不包含任何C++扩展,因为它不包含任何特定平台的二进制文件。本地构建的版本将包含这些。这些扩展仅在您想利用某些优化或想运行进程内JVM时才需要,没有它们也可以正常工作。
一个简单的示例
框架可以通过多种方式启动,以便客户端连接
- 向现有的Java进程中添加PJRmi线程,Python客户端可以通过网络连接
- 在Python进程中实例化Java JVM,Python进程是唯一的连接
- 从Python进程内部启动Java子进程,父进程是唯一的连接
- 在Python进程中生成Java JVM。
下面是最后一个示例。
$ python
>>> import pjrmi
>>> c = pjrmi.connect_to_child_jvm()
现在c
是一个PJRmi
实例,可以用来从服务器请求信息。基本上有两种方法可以获取信息:您可以得到一个Java对象的引用,或者一个Java类的引用。还可以参考Python接口中的各种其他connect_to_blah()
方法。
有了这个连接,我们可以获取类定义,创建其实例并调用其实例上的方法
>>> ArrayList = c.javaclass.java.util.ArrayList
>>> a = ArrayList([1,2,3])
>>> a.size()
3
>>> a.toString()
'[1, 2, 3]'
>>> a.get(1)
2
>>> a.hashCode()
30817
>>> a.contains(1)
True
您可以将Java对象视为Python对象一样使用
>>> str(a)
'[1, 2, 3]'
>>> hash(a)
30817
>>> sum(a)
6
>>> 1 in a
True
hypercube
包支持类似ndarray
的Java类,同时提供了一部分numpy
数学库操作。Hypercubes还作为Python中的ndarray
进行鸭子类型。
>>> DoubleArrayHypercube = c.class_for_name('com.deshaw.hypercube.DoubleArrayHypercube')
>>> Dimension = c.class_for_name('com.deshaw.hypercube.Dimension')
>>> CubeMath = c.class_for_name('com.deshaw.hypercube.CubeMath')
>>> dac = DoubleArrayHypercube(Dimension.of((3,3,3)))
>>> dac.reshape((27,))[:] = tuple(range(27))
>>> dac[0]
DoubleSlicedHypercube([ [0.0, 1.0, 2.0] ,
[3.0, 4.0, 5.0] ,
[6.0, 7.0, 8.0] ])
>>> numpy.sum(dac)
351.0
>>> CubeMath.sum(dac)
351.0
这些在Jupyter notebook中有更详细的说明。
访问现有对象实例
>>> foo = c.object_for_name('Foo')
您可以使用命令 c.object_for_name(<字符串>)
获取 Java 对象的引用。这将调用服务器上的一个 Java 方法,该方法会查看字符串并决定传递哪个对象。在这个例子中,它将简单地返回 None(或 Java 视角中的 null
)。此方法在 PJRmi
类中未实现 —— 每个服务器都必须实现它并决定您想向 Python 暴露哪些对象。在 com.deshaw.PJRmi.main
中的独立测试服务器示例只是这样做
// Create a simple instance which just echoes back the name it's given
final PJRmi pjrmi =
new PJRmi("PJRmi", provider) {
@Override protected Object getObjectInstance(CharSequence name) {
return name.toString().intern();
}
};
大多数情况下,Java 对象以原样返回,但在本例中,foo
将是 Python 字符串的一个特殊子类;因此,如果我们想得到 Java String,我们需要将其提取出来。Java 对象通常以对象形式返回,只有字符串和原始类型(int
、double
、boolean
、...)有特殊的包装处理。
>>> foo = c.object_for_name('Foo')
>>> foo
u'Foo'
>>> foo = foo.java_object
>>> foo
<pjrmi.java.lang.String at 0x39e7c10>
>>> foo.<tab>
foo.CASE_INSENSITIVE_ORDER foo.endsWith foo.lastIndexOf foo.startsWith
foo.charAt foo.equals foo.length foo.subSequence
foo.codePointAt foo.equalsIgnoreCase foo.matches foo.substring
foo.codePointBefore foo.format foo.notify foo.toCharArray
foo.codePointCount foo.getBytes foo.notifyAll foo.toLowerCase
foo.compareTo foo.getChars foo.offsetByCodePoints foo.toString
foo.compareToIgnoreCase foo.getClass foo.regionMatches foo.toUpperCase
foo.concat foo.hashCode foo.replace foo.trim
foo.contains foo.indexOf foo.replaceAll foo.valueOf
foo.contentEquals foo.intern foo.replaceFirst foo.wait
foo.copyValueOf foo.isEmpty foo.split
>>> foo.getBytes?
Type: function
String Form:<function getBytes at 0x5fdeb90>
File: .../pjrmi.py
Definition: foo.getBytes(*args, **kwargs)
Docstring:
A wrapper for the Java method:
java.lang.String#getBytes()
taking the following forms:
[B getBytes()
[B getBytes(java.lang.String)
[B getBytes(java.nio.charset.Charset)
void getBytes(int, int, [B, int)
当 Python 客户端收到回复时,它会收到一个 ID,这是一个对象的句柄,以及对象类型。如果它以前没有看到过这种类型,它将要求 Java 服务器获取类型信息,包括类层次结构和所有可用的方法。它将使用这些信息来提供 ipython 自动完成和文档。
>>> foo._<tab>
foo.__add__ foo.__doc__ foo.__len__ foo.__repr__ foo._bases foo._is_immutable
foo.__class__ foo.__eq__ foo.__module__ foo.__setattr__ foo._classname foo._is_primitive
foo.__cmp__ foo.__format__ foo.__ne__ foo.__sizeof__ foo._handle foo._pjrmi
foo.__del__ foo.__getattribute__ foo.__new__ foo.__str__ foo._hash_code foo._type_id
foo.__delattr__ foo.__hash__ foo.__reduce__ foo.__subclasshook__ foo._instance_of
foo.__dict__ foo.__init__ foo.__reduce_ex__ foo.__weakref__ foo._is_array
>>> foo._is_immutable
True
>>> str(foo)
'Foo'
>>> foo._str
'Foo'
使用类创建新对象实例
两种不同的方式来获取 Java 类作为 Python 类的句柄
>>> byte_array = c.class_for_name('[B')
>>> String = c.javaclass.java.lang.String
c.class_for_name(<字符串>)
,以及其语法糖等价物,返回一个 Java 类对象的句柄,可用于调用静态方法。这将返回它知道的任何类,而无需特殊处理(与 object_for_name
不同,对于后者,服务器必须有一种将每个输入字符串映射到对象的方法)。
然后您可以使用此结果创建新实例
>>> ArrayList = c.javaclass.java.util.ArrayList
>>> ArrayList([1,2,3])
<pjrmi.java.util.ArrayList at 0x378a650>
>>> str(_)
'[1, 2, 3]'
请注意,这些类已经完全填充了方法和之类的功能,因此自动完成在这些类上工作,并且也存在反射生成的文档字符串
>>> ArrayList.<tab>
ArrayList.add ArrayList.containsAll ArrayList.hashCode ArrayList.listIterator ArrayList.removeAll ArrayList.toArray
ArrayList.addAll ArrayList.ensureCapacity ArrayList.indexOf ArrayList.mro ArrayList.retainAll ArrayList.toString
ArrayList.clear ArrayList.equals ArrayList.isEmpty ArrayList.notify ArrayList.set ArrayList.trimToSize
ArrayList.clone ArrayList.get ArrayList.iterator ArrayList.notifyAll ArrayList.size ArrayList.wait
ArrayList.contains ArrayList.getClass ArrayList.lastIndexOf ArrayList.remove ArrayList.subList
>>> ArrayList.add?
Type: function
String Form:<function add at 0x37f6de8>
File: .../pjrmi/__init__.py
Definition: ArrayList.add(*args, **kwargs)
Docstring:
A wrapper for the Java method:
java.util.ArrayList#add()
taking the following forms:
boolean add(java.lang.Object)
void add(int, java.lang.Object)
Python 和 Java 值的自动转换
在上面的例子中,您会注意到我们向 ArrayList
构造函数传递一个 Python 元组。因为其中一个构造函数的形式是
ArrayList(Collection<? extends E> c)
PJRmi 代码知道尝试将 tuple
作为 Collection
进行打包。同样,由于 ArrayList
是一个 Iterable
,PJRmi 代码也知道它可以在 for 循环中遍历它
>>> a = ArrayList([1,2,3])
>>> for i in a:
... print(i)
...
1
2
3
也会尝试在飞地转换其他类似类型。
异步方法调用
从 Python 调用 Java 方法是可能的,以便在稍后的时间点收集其结果。该方法将在工作线程中调用,异步调用将返回一个 Java Future
以最终收获结果
>>> Thread = c.class_for_name('java.lang.Thread')
>>> l = tuple(Thread.sleep(10000, __pjrmi_sync_mode__=c.SYNC_MODE_JAVA_THREAD) for i in range(10))
>>> c.collect(l)
# You wait, time passes...
(None, None, None, None, None, None, None, None, None, None)
这些调用使用的工作线程具有以下属性
- 它们与回调线程不同。
- 它们是长期存在的。
- 每个都有一个唯一的 ID,从锁定语义的角度来看。
请注意,Java 服务器将保留 Future
中存储的结果,直到调用 collect()
。因此,如果在收获结果之前进行了太多的调用,堆可能会耗尽。
按值操作
由于对 Java 对象的操作涉及往返服务器,有时取 Java 值的副本作为其等效 Python 值可能更有效率。这使用 Java 代码 PythonPickle
完成,由 Python 侧的 pickle
读取。
例如,Java 数组可以转换为 Python 数组
>>> double_a = c.class_for_name('[D')
>>> array = double_a(100)
>>> for i in range(len(array)):
... array[i] = i
>>> type(array)
pjrmi.[D
>>> array[10]
10.0
>>> python_array = c.value_of(array)
>>> type(python_array)
numpy.ndarray
>>> python_array[10]
10.0
但请记住,这仅仅是 Java 值的一个副本;对 Java 的更改不会反映在 Python 上。
>>> for i in range(len(array)):
... array[i] = 10 * i
>>> array[10]
100.0
>>> python_array[10]
10.0
您还可以使用 best_effort
转换获取容器及其元素的混合版本
>>> Object = c.class_for_name('java.lang.Object')
>>> lst = ArrayList([Object() for _ in range(3)])
>>> lst.toString()
'[java.lang.Object@1534bbf7, java.lang.Object@47c234d1, java.lang.Object@657cd6d0]'
>>> c.value_of(lst, best_effort=True)
[<pjrmi.java.lang.Object at 0x7f3a7bedb940>,
<pjrmi.java.lang.Object at 0x7f3a7bedb3a0>,
<pjrmi.java.lang.Object at 0x7f3a68698130>]
这可能很有用,例如,这意味着 Python 客户端不需要创建并调用 Java 迭代器的方法来遍历列表。
类型推断
有时我们可能需要从Python类型推断Java类型。这可能在方法接受Java Object
或泛型类型信息因类型擦除而丢失时发生。
例如,想象一下我们有一个Java方法,它接受一个 Set<Long>
import java.util.Set;
public class Foo
{
public static String toString(Set<Long> set)
{
StringBuilder result = new StringBuilder();
for (Long l : set) {
if (result.length() > 0) {
result.append(',');
}
result.append(l);
}
return result.toString();
}
}
有人可能会认为这样做会起作用,但实际上并不起作用
>>> s = set(range(10))
>>> Foo.toString(s)
[...]
java.lang.ClassCastException: java.lang.ClassCastException: java.lang.Byte cannot be cast to java.lang.Long
at Foo.toString(Foo.java:8)
原因在于Python只知道它有一个整数集合,它对它们一无所知,因此它只能做出最好的猜测;在这里,由于所有值都可以放入一个字节中,它使用字节来表示它们。
我们可以通过在Python内部提供类型信息来解决这个问题
>>> s = set(numpy.arange(10, dtype='int64'))
>>> Foo.toString(s)
u'0,1,2,3,4,5,6,7,8,9'
类型转换
>>> ArrayList = c.class_for_name('java.util.ArrayList')
>>> a = ArrayList([1,2,3])
Python和Java之间存在一定程度的自动转换。例如,在这里PJRmi看到Python列表被自动转换为Java Collection以创建一个 ArrayList
。
>>> a0 = a[0]
>>> a0
1
>>> a0.java_object
<pjrmi.java.lang.Byte at 0x378a5d0>
在运行时,泛型对象(如 ArrayLists
)不携带它们包含的对象的类型信息。因此,具有泛型类型的方法返回的对象被转换为其实际值。在这里,它恰好是一个包装在Python int中的 Byte
,但它也可能是这样的
>>> a = ArrayList([ArrayList(), ArrayList(), ArrayList()])
>>> a[0]
<pjrmi.java.util.ArrayList at 0x378a6d0>
但是,如果您需要将一种类型实际转换为另一种类型,则可以使用 cast_to()
方法
>>> ArrayList = c.javaclass.java.util.ArrayList
>>> List = c.javaclass.java.util.List
>>> a = ArrayList([1,2,3])
>>> c.cast_to(a, List)
<pjrmi.java.util.List at 0x378ac90>
扩展对其他类型的支持
有时用户可能希望PJRmi理解如何将Python和Java中的用户定义类型进行转换。
让我们想象一下,在(编辑过的)示例中,int
和String
是特殊类型
>>> Integer = c.class_for_name('java.lang.Integer')
>>> Integer.parseInt(1)
TypeError: Could not find a method matching java.lang.Integer#parseInt(<class 'int'>): Don't know how to turn '1' <class 'int'> into a <java.lang.String>
PJRmi可以通过重写 PJRmi._format_by_class
方法来扩展以理解如何从特定的Python类型转换为特定的Java类型。我们创建一个可以在Java端调用的lambda,将给定的值转换为所需的Java对象。请注意,不会进行任何类型检查以确保Java方法返回的对象是所需的。
>>> class MyPJRmi(pjrmi.PJRmi):
... def connect(self):
... super().connect()
... # Remember these classes so we can use them later. We capture
... # these after we have connected the the Java process.
... self._my_java_lang_String = self.class_for_name('java.lang.String')
... self._my_java_lang_Object = self.class_for_name('java.lang.Object')
...
... def _format_by_class(self, klass, value,
... strict_types=True, allow_format_shmdata=True):
... try:
... return super()._format_by_class(klass, value,
... strict_types=strict_types,
... allow_format_shmdata=allow_format_shmdata)
... except Exception:
... # See if we are trying to marshall the value as a String
... if klass._type_id == self._my_java_lang_String._type_id:
... # Turn the value into a String on the Java side by invoking
... # the String.valueOf(Object) method on it
... method = self._my_java_lang_String.valueOf[self._my_java_lang_Object]
... return super()._format_as_lambda(method, value,
... strict_types=strict_types,
... allow_format_shmdata=allow_format_shmdata)
... else:
... raise
>>> c = pjrmi.connect_to_child_jvm(stdout=None, stderr=None, impl=MyPJRmi)
>>> Integer = c.class_for_name('java.lang.Integer')
>>> Integer.parseInt(1)
1
lambda和鸭子类型类
PJRmi还支持将Python函数用作Java lambda,以及实现Java接口的Python类。为了做到这一点,Java端必须能够调用Python端(通常是相反的);这意味着我们需要使用多线程、基于工作者的对。您可以从Python内部这样启动一个Java子进程,其中包含两个工作者
>>> c = pjrmi.connect_to_child_jvm(
>>> stdout=None,
>>> stderr=None,
>>> application_args=('num_workers=2',)
>>> )
现在,我们将创建一个 Map
并对其调用 computeIfAbsent() 方法,使用Python lambda作为提供函数
>>> HashMap = c.class_for_name('java.util.HashMap')
>>> m = HashMap()
>>> m.computeIfAbsent(1, lambda x: x + 1)
2
类似地,我们可以使用Python类实现Java接口,并将该类传递给函数。只要Python类中存在所有必需的方法,就可以将其用作接口的实现。PJRmi中的 JavaProxyBase
类提供了标准所需的方法(如 equals()
和 hashCode()
),实际接口方法由您自己创建。在这里,我们将实现一个Java Runnable
并将其传递给Java Thread
以调用
>>> Thread = c.class_for_name('java.lang.Thread')
>>> class PythonRunnable(pjrmi.JavaProxyBase):
... def run(self):
... print("I ran!")
>>> runnable = PythonRunnable()
>>> runnable.run()
I ran!
>>> thread = Thread(runnable)
>>> thread.start()
I ran!
可以在Java服务器中重写 protected int PJRmi.numWorkers()
方法以提供对PJRmi服务器进程的回调支持。
原生/CPython数组处理
PJRmi可以使用相对快速的 C++
机制处理按引用传递的数组,具体为 memcpy()
和 mmap()
。这导致数据传输速度从Java到Python和从Python到Java显著提高。目前,此功能仅适用于子进程,因为它通过 /dev/shm
隧道数据。从Python端,通过在建立连接时将 kwarg_use_shm_arg_passing
设置为 True
来启用此功能
>>> _pjrmi_connection = pjrmi.connect_to_child_jvm(use_shm_arg_passing=True, ...)
在Java中,当启动 PythonMinion
时,将 useShmArgPassing
标志设置为 true
。
1 private static final PythonMinion PYTHON = PythonMinionProvider.spawn(true)
2 // or
3 private static final PythonMinion PYTHON = PythonMinionProvider.spawn(myStdinFilename,
4 myStdoutFilename,
5 myStderrFilename,
6 true)
动态Java源注入
PJRmi支持从Python中的str
动态编译Java源代码。这可以消除通过PJRmi进行特别“健谈”通信的需求;例如,当从Python中遍历Java ArrayList<HashMap<Integer,Integer>>
对象时。编译使用JavaCompiler
,可以使用如下方式
>>> class_name = "TestInjectSource"
>>> source = """
public class TestInjectSource {
public static int foo(int i) {
return i+1;
}
}
"""
>>> Foo = c.inject_source(class_name, source)
>>> foo = Foo()
>>> foo.foo(1)
2
Java方法捕获
PJRmi支持Java方法捕获,允许它们作为FunctionalInterface
(即lambda)参数传递。它们也可以独立使用,就像Python中捕获的方法一样。
糖语法如下。切片或省略号是通配符匹配,但只有在没有重载(即不需要歧义消除)的情况下才能使用。
语法 | 描述 |
---|---|
Class.method[None] |
无参数的方法 |
Class.method[:] |
带有一些,但任何参数的方法 |
Class.method[...] |
带有一些,但任何参数的方法 |
Class.method[t0, t1, ...] |
显式定义参数的方法 |
例如,Java Map有一个接受lambda的方法
public V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
并可以相应使用
>>> HashMap = c.class_for_name('java.util.HashMap')
>>> Integer = c.class_for_name('java.lang.Integer')
>>> jint = c.class_for_name('int')
>>> m = HashMap()
>>> m.computeIfAbsent(12345678, Integer.toString[jint])
'12345678'
>>> m
{12345678=12345678}
与Python一样,从实例捕获的方法将与该实例关联。当作为lambda使用时,从类捕获的方法要么必须是静态的,要么需要通过传递this
指针来调用。
>>> m = HashMap()
>>> m.put(1,2)
>>> m
{1=2}
>>> p = m.put[:]
>>> p(2,3)
>>> m
{1=2, 2=3}
>>> m.computeIfAbsent('hello', String.hashCode[None])
99162322
>>> m
{1=2, 2=3, hello=99162322}
使用Java捕获方法,可以避免因反复从Python侧调用Java而导致的调用开销。例如,当我们对Java侧执行映射操作时,我们只需进行一次调用,而不是多次。
# Get a list of Integers, accounting for the fact that they are a boxed type
# on the Python side
>>> l = list(Integer.valueOf(i).java_object for i in range(100000))
# Apply the instance method toString() on each of them, from the Python side
>>> %time _ = list(map(Integer.toString[None], l))
CPU times: user 2.74 s, sys: 383 ms, total: 3.13 s
Wall time: 3.03 s
# Apply the same method, but all on the Java side. We assume that we have a
# function like this in a special utility class:
# public static <T,U> List<U> map(final Collection<T> c,
# final Function<T,U> f)
# which we will use.
>>> %time _ = CollectionUtilities.map(l, Integer.toString[None])
CPU times: user 130 ms, sys: 2.94 ms, total: 133 ms
Wall time: 153 ms
方法构造函数可以通过get_bound_method()
方法显式捕获,或通过使用方括号的语法糖来捕获。传递参数的Java类型可用于消除重载方法的歧义。显式捕获和糖都接受类实例或Java的类型名称作为参数。
>>> str(c.get_bound_method(Integer.toString, arg_types=(jint,)))
'java.lang.Integer::toString'
>>> str(c.get_bound_method(Integer.toString, arg_types=('int',)))
'java.lang.Integer::toString'
>>> str(Integer.toString[jint])
'java.lang.Integer::toString'
>>> str(Integer.toString['int'])
'java.lang.Integer::toString'
捕获的方法也可以用于处理重载歧义
>>> l = list(range(10))
>>> Arrays.binarySearch(l, 5)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [14], in <cell line: 1>()
----> 1 Arrays.binarySearch(l, 5)
[...]
TypeError: Call to binarySearch(<class 'list'>, <class 'int'>) is ambiguous; multiple matches: binarySearch([B, byte), binarySearch([J, long), binarySearch([I, int), binarySearch([S, short), binarySearch([Ljava.lang.Object;, java.lang.Object), binarySearch([D, double), binarySearch([F, float)
>>> Arrays.binarySearch['[I', 'int'](l, 5)
5
技术说明
C扩展
通过C扩展提供扩展功能,这两种功能都在Java和Python代码中提供。
- 进程内JVM:这是在Python进程内部启动Java VM。虽然这目前有效,但它高度依赖于Python和Java VM之间的良好协作,因此可能会在明天失败。不推荐使用。
- SHM参数传递:这是一种优化,其中使用
/dev/shm
通过memcpy()
传递数组参数;因此,它仅在Java和Python进程位于同一主机上时才有效。对于一般用途不是必需的。
在PyPI(https://pypi.ac.cn/project/pjrmi/)中可用的PJRmi构建只包含平台无关的Python代码和Java JAR,不包含C扩展。
安全模型
可以使用SSL密钥控制从远程Python客户端到Java服务器的访问。这适用于任何部署环境,因为一旦连接,客户端实际上就在服务器进程内部,可以执行任意代码。
上述情况的限制是服务器可能选择有一个类允许列表,该列表限制了Python客户端可以访问的Java类集合。这限制了客户端的能力,因为它们可能无法访问允许启动子进程等的类。这可以通过在PJRmi子类中重写isClassBlockingOn()
和isClassPermitted()
方法在服务器进程中控制。
线程模型
PJRmi服务在Java进程内部作为一个单独的线程运行。因此,您必须考虑任何线程安全问题。框架提供对ReentrantLock
的支持,这有助于在调用期间持有锁。
需求
PJRmi使用Java11及更高版本和Python 3.6及更高版本的功能。
历史
PJRmi是由D. E. Shaw集团贡献给社区的。
许可证
该项目在BSD-3-Clause许可证下发布。
项目详细信息
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。
源分布
构建分布
pjrmi-1.13.10.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 123b6f2ed6cfaa0a3d73556960d353dec9c8c5f062b564211adc86c94d5742ed |
|
MD5 | cf4bc17d71584ca57703547c70a3892d |
|
BLAKE2b-256 | d15cda20f7a5a16bc6c429bcc68c4145a9d3fea207bd8aec526a6d17af350852 |
pjrmi-1.13.10-py3-none-any.whl 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 9ebee8c50f4e20827ad9afa1b3a0f98511448977552cb7328e84613b0086bab7 |
|
MD5 | c7979f30a5d0462c0319c2c9df8a6743 |
|
BLAKE2b-256 | 654298ca0e1d311118c5b6d4dddbc6e83e1aa916b5d73d0c967f984b2b72a624 |