之前负责的大数据项目里,由于客户提供的云环境过于特殊,Spark 的版本太老,用不上 Apache Livy ,迫于无奈只能自己实现一个较为直接(菜鸟)的 Spark Restful 交互,当时有一个难点就是需要将 PySpark 运行的某些信息额外存一份到 HDFS 上。当然 RDD 存 HDFS 上对于 Spark 来说是非常方便,但是却怎么也找不到 PySpark 直接操作 HDFS 的方法,当时是参考了 Raven’s Blog 的一篇文章解决了,也了解到了 PySpark 用于操作 Java 对象的库 Py4J ,然后心血来潮,稍微归纳一下目前自己用到或者摸过的 Python 和其他语言的互操作库,纯粹分享(水一篇 blog)。
和 C/C++ 交互
大部分人用的都是 CPython ,因此和 C 进行交互是最为直接的,而 ctypes
本身也包含在了 Python 发行版的标准库里,可见其重要性。Python 和 C/C++ 交互无法是为了速度,或者保密需要,再者就是纯粹的胶水脚本。虽说是与 C/C++ 进行交互,实际上 Python 本身只支持 C API ,因此本质上还是和 C 交互。ctypes
的资料确实不算多,但对比起其他环境来已经是好太多,本质上还是这个交互过程实际上也较为依赖系统编程的知识点,因此即使文档再详细,没有相关的知识储备读起来还是晕头转向。
对于 C++ 的函数接口,需要通过
extern
声明转换为 C 的函数接口,这样编译出来的 dll 才能被 Python 调用得到。
ctypes
在不同的操作系统上会使用对应的动态加载动态链接库的方法,并通过一套映射方法将 Python 和二进制形式的动态链接库连接起来。在 Windows 下最终调用的是 Windows API 的 LoadLibrary
和 GetProcAddress
,而在 Linux 和 Mac OSX 下则是 Posix 标准的 dlopen
和 dlsym
。其中 ctypes
实现了一系列的类型转换,将 Python 类型转为 C 类型作为调用参数,然后将返回结果从 C type 转回为 Python type 。
下面这个例子纯粹是演示用途(实际上并不能跑) 😂 ,同时也是提醒 ctypes 的跨平台性较差,使用的时候需注意。
1 | # 1. 引入 ctypes 库 |
至于其他的类型映射,如数组、指针、结构体、联合体等请查阅 ctypes
文档。
和 .NET 交互
.NET 平台上的语言基本上都要在 CLR 上运行(这里不包括新的 CoreCLR),当然了最直接肯定是用 IronPython ,让 Python 在 CLR 运行不就把问题解决了吗 😀 ?但是 Python 中 C 依赖较多的库是没有办法在 IronPython 上使用,同时目前看来 IronPython3 的步伐既跟不上 Python 也跟不上 .NET (这也很无奈,两边都是高速往前,而 IronPython 组的核心成员也不够稳定,希望喜欢 IronPython 的能多支持一下),所以折衷的方案就是使用 Python for .NET 了。
Python for .NET 的安装很简单:pip install pythonnet
。
下面摘自 PyPI 的例子,咋一看和 IronPython 引入第三方 .NET DLL 也非常类似:
1 | # Python for .NET 对于 CLR 的命名空间将视为 Python 的 Package |
更多的教程可以取查看 Python for .NET 的 github page (详见参考资料),因为支持 .NET Framework 和 mono 的平台,所以 Python For .NET 的跨平台性还算可以,也支持内嵌 Python 到 .NET 中(PyPI 上同样由 C# 引用 Python 的例子),这个可以说是很惊喜了。
和 Java 交互
类似于 IronPython ,同样也有在 JVM 上运行的 Python 版本:Jython 。对于 Jython 我对它的了解确实不够多,对于 JVM 的语言的了解除了 Java 和浅尝则止的 Scala 以及 Kotlin 就很有限了。也是在做 PySpark 这边的项目才知道 PySpark 是通过 Py4J 的手段连接 Python 和 Java 。
Py4J 可以使运行在 Python Interpreter 的 Python 程序能够动态访问 Java 虚拟机的对象,同样 Java 也可以回调 Python 的对象。
使用 Py4J 的过程有点曲折,详细可以查看它的官网介绍(见「参考资料」一节),按它的说法是 an hybrid between a glorified Remote Procedure Call and using the Java Virtual Machine to run a Python program.,所以说本质上是基于 socket 的 RPC 调用,因此对比 Jython 在 JVM 上执行 Python 代码和 JPype 这种通过 JNI 通信的来说开销会更大,但是也更独立(也就是 Python 可以使用原先 Cpython 能使用的库而不受影响),对性能有格外要求的可以忽略了(不过我寻思都用这两了在性能上就不要强求了吧😂)。
注意:官网的 Installing 环节没有直接提供 py4j 的 jar 包下载,可以下载 py4j-java 的源码然后通过 gradle 和 maven 来构建,这里我用的 maven ,然后将 py4j-0.10.9.jar 包的依赖加到 maven 项目中。
这里直接上了一下 Welcome 页的例子
Python 代码部分:
1 | from py4j.java_gateway import JavaGateway |
Java 代码部分:
1 | import py4j.GatewayServer; |
注意由于是 RPC 调用的关系 Java 部分代码要求先于 Python 代码部分运行,且两者需同时运行才能正常运行,换言之, Py4J 是不会启动 JVM 的 。
和 Lua 交互
多年前为了探讨一下解决 CPython GIL 锁的限制,考虑过在并发上引入 Lua 的方案,然后就发现了 Lupa
这个东西。其实 Lupa
还有个前身 lunatic-python
,这个东西主要提供了 Python 和 Lua 之间的桥接,既能让 Python 运行 Lua ,又能让 Lua 运行 Python ,机制就是在各自的 interpreter 创建对方的 interpreter 。Lupa
就是将 Lua 或 LuaJIT2 的运行时整合进 CPython 中,同时重写了部分 lunatic-python
内容以更好地提供 coroutine 的支持。
这里 Lua 实际上是 Python 的一种补充,由于 Lua 的运行时很小(700K左右便于嵌入)而 LuaJIT 可以将动态的 Lua 编译成极快的机器代码,所以可以将 Lua 作为 Python 的一种加速且资源友好的 bakcup language ,而 Lupa
就是其中的包装器包装了两个 runtime 交互的细节。
至于其他的用途在 PyPI 的页面也提到了:
- 使用 Python 协程装饰的 Lua 协程
- 正确的处理字符串的编码和解码(对于 Python2 比较有用)
- 调用 lua 时会释放 GIL 并支持在单独的运行时中进行线程化
- 面向 LuaJIT 编写,不过 lua 的 interpreter 可以替换为标准的 Lua 5.1 和 Lua 5.2
- 因为使用 Cython 编写而不是 C 所以便于扩展(相对而言吧)
PyPI 中 “Installing lupa” 一节中介绍了使用 LuaJIT2 和 Lua5.1 的 lupa 在各个平台的源码编译安装过程,pip install 的默认是基于 LuaJIT2 运行是。
这里演示的是多线程生成 Mandelbrot 集合的图象并用 PIL 库展示的代码(来自 PyPI)
1 | lua_code = '''\ |
注意这里实际上为每个线程都创建一个单独的 LuaRuntime 并行执行,每个 LuaRuntime 都有一个全局锁保护,防止并发访问。因为 Lua 内存占用低,因此可以使用多个运行时,但这种设置也意味着不能轻易在 Lua 内部的线程之间交换值,必须通过 Python 的空间复制来传递。
和 JavaScript 交互
Python 和 JS 交互最常见的场面就是在爬虫开发上了,越来越的网页对 JavaScript 进行了混淆,如果懒得去破的话直接运行 JavaScript 反倒是最快的。
当然这种情况要用到 client-side 对象的话 selenium 之类的可能更直接,如果只是用到了 js 环境,那么比如 PyExecJS 和 PyV8都可以用一下。然而实际情况是,PyExecJS 已经 EOL 了,PyV8 也已经咸鱼好久了 ……
单纯运行 JavaScript 方面其实也没有什么大的进展,反倒是像 Js2Py 这一种将 JavaScript 翻译为 Python 的还是很活跃的,而且号称无依赖且完全支持 ECMAScript 5.1 (ECMA 6 支持尚在试验阶段),不过对于混淆很深的 JS 代码或者有外部 node 依赖的性能也不会好到哪里去。
比方说提供对 ECMA6 支持是通过 Babel 将 ECMA6 转为 ECMA5.1 再通过 Js2Py 将 ECMA5.1 转为 Python ,其中导入 babel.js (4M 大小) 转译这过程就要 15 秒 ……
最后看一下 Js2Py 的代码(这个基本和交互就没关系了)
1 | import js2py |
参考资料
- ctypes — A foreign function library for Python — Python 3.7 documentation
- 聊聊Python ctypes 模块 - 知乎
- ctypes (Operating System) - Python 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云
- pythonnet · PyPI
- Python for .NET | pythonnet.github.io
- .NET/C# Interop to Python - Stack Overflow
- Welcome to Py4J — Py4J
- py4j——解决用python访问java遇到的问题 - 简书
- Lunatic Python - Labix
- lupa · PyPI
- PiotrDabkowski/Js2Py: JavaScript to Python Translator & JavaScript interpreter written in 100% pure Python🚀