最近在 Github
上发现一个非常有意思的项目 PythonMonkey
,它可以让我们直接在 JavaScript
中运行 Python
代码,也可以在 Python
中运行 JavaScript
和 WebAssembly
代码,而且几乎没有性能损失。
PythonMonkey
是一个 Python
库,它使用 Mozilla
的 SpiderMonkey
JavaScript
引擎构建,可以实现 Python
和 JavaScript
之间的互操作。它可以让 JavaScript
库能够在 Python
代码中无缝使用,反之亦然,而不会造成比较大的性能损失。例如,我们可以从 JavaScript
库中调用 NumPy
等 Python
包,或者直接从 Python
中使用 crypto-js
等 NPM 包。此外,使用 WebAssembly API
和 SpiderMonkey
引擎在 Python
中执行 WebAssembly
模块也变得非常简单。开发者也可以使用 PythonMonkey
重构用 Python
编写的代码改为在 JS 中执行,利用 SpiderMonkey
的即时编译器获得接近原生的速度。
PythonMonkey
还附带了 PMJS
,这是一个类似于 Node.js
的 JavaScript
运行时环境,支持从 JavaScript
调用 Python
库。
下面是一个简单的 “hello world”示例,演示了从 JavaScript
生成的字符串并返回到 Python
上下文:
>>> import pythonmonkey as pm >>> hello = pm.eval(" 'Hello World'.toUpperCase(); ") >>> print(hello) 'HELLO WORLD'
下面是一个复杂一点的示例,演示了将 Python print
函数作为参数传递给 JavaScript
函数,然后从 Python
调用该 JavaScript
函数:
>>> import pythonmonkey as pm >>> hello = pm.eval("(func) => { func('Hello World!')}") >>> hello(print) Hello World!
通过下面的写法我们可以直接在 JavaScript
代码中使用 Python
的 print
函数:
const pyPrint = python.eval("print"); pyPrint("Hello, World!"); // this outputs "Hello, World!"
我觉得一个比较实用的应用场景就是我们可以轻松地将一个 JavaScript
库移植到 Python
,而不需要承受使用 Python
重写库和维护迁移的巨大成本。JavaScript
比 Python
处理异步代码的能力要好很多。我们只需要做下面这样简单的操作:
my-javascript-module.js
exports.sayHello = () => { console.log('hello, world') };
main.py
import pythonmonkey as pm test = pm.require('./my-javascript-module'); test.sayHello() # this prints hello, world
同样的,我们可以从 JavaScript
中通过 CommonJS
加载一个 Python
模块:
my-python-module.py
def getStringLength(s): return len(s) exports['getStringLength'] = getStringLength
my-javascript-module.js
`const { getStringLength } = require('./my-python-module'); function printStringLength(s) { console.log(`String: "${s}" has a length of ${getStringLength(s)}`); } module.exports = { printStringLength, }; `
main.py
import pythonmonkey as pm test = pm.require('./my-javascript-module'); test.printStringLength("Hello, world!") # String: "Hello, world!" has a length of 13
PythonMonkey
还利用了一些其他的 SpiderMonkey
功能,例如它的 WebAssembly
(WASM
) 引擎,它可以允许 Python
在沙箱中运行来自各种语言(例如 C、C++、Rust
等)的不受信任的 WASM
代码。
在 Python 中调用 WebAssembly 函数:
import asyncio import pythonmonkey as pm async def async_fn(): # read the factorial.wasm binary file file = open('factorial.wasm', 'rb') wasm_bytes = bytearray(file.read()) # instantiate the WebAssembly code WebAssembly = pm.eval('WebAssembly') wasm_fact = await WebAssembly.instantiate(wasm_bytes, {}) # return the "fac" factorial function from the wasm module return wasm_fact.instance.exports.fac; # await the promise which returns the factorial WebAssembly function factorial = asyncio.run(async_fn()) # execute WebAssembly code in Python! print(factorial(4)) # this outputs "24.0" since factorial(4) == 24 print(factorial(5)) # this outputs "120.0" print(factorial(6)) # this outputs "720.0"
还有更多的示例,我们可以查看 Github 上的例子:https://github.com/Distributive-Network/PythonMonkey-examples
目前已经有几个用于在 Python
中运行 JavaScript
的项目了,例如 JS2PY
、PyV8
和 Metacall
。
JS2Py
完全用 Python
实现,它消除了对 V8
或 SpiderMonkey
等大型引擎的需求。但是这种方法也有一些问题,如果不利用现有的 JavaScript
引擎,JS2Py
就无法从 V8
或 SpiderMonkey
等引擎在数百万人每天使用的浏览器中提供的强大、不断更新且经过验证的代码库中受益。此外,JS2Py
还缺少 WASM
引擎、对最新 JavaScript
规范 (ECMA-262
) 的支持以及这些引擎内置的强大 JIT
等功能。现代异步 JS
编程中广泛使用的 JavaScript Promises
和 Async/Await
在 JS2Py
中也是缺失的,但在 PythonMonkey
中是可用的。使用 Python
编写,JS2Py
面临 SpiderMonkey
中不存在的性能限制;在 SunSpider JavaScript
基准测试报告显示:使用 PythonMonkey
比 JS2Py
快了 1162.5
倍。
PyV8
和 Cloudflare
的现代实现是 Google V8 JavaScript
引擎绑定的 Python
包装器。这意味着它的运行级别比 PythonMonkey
更低,并且不支持事件循环功能,例如 JavaScript
的 Promise
和 async/await
。
Metacall
是一个可扩展、可嵌入和可互操作的跨平台多语言运行时,可与多种编程语言(例如 JavaScript、Python、Ruby、Rust、C#、Java
等)进行互操作。但是 Metacall
支持的广泛支持语言也是有代价的,并且需要在 Python
包之外的系统上安装额外的软件才能运行。此外,Metacall
会复制在 Python
和 JavaScript
之间传递的数据,而不是像 PythonMonkey
那样通过引用传递,从而导致性能影响。
虽然替代项目与 PythonMonkey
的模型有相似之处,但它们达不到 PythonMonkey
提出的互操作性、易用性和速度。