Python与Javascript相互调用超详细讲解(二)基本原理Part 2 - 通过翻译/解释副语言
首先要明白的是,javascript和python都是解释型语言,它们的运行是需要具体的runtime的。
- Python: 我们最常安装的Python其实是cpython,它有一个基于C的解释器。除此之外还有像pypy这种解释器,等等。基本上,不使用cpython作为python的runtime的最大问题就是通过pypi安装的那些外来包,甚至有一些cpython自己的原生包(像
collections
这种)都用不了。 - JavaScript: 常见的运行引擎有google的V8,Mozilla的SpiderMonkey等等,这些引擎可以把JavaScript代码转换成机器码执行。基于这些基础的运行引擎,我们可以开发支持JS的浏览器(比如Chrome的JS运行引擎就是V8);也可以开发功能更多的JS运行环境,比如Node.js,相当于我们不需要一个浏览器,也可以跑JS代码。有了Node.js,JS包管理也变得方便许多,如果我们想把开发好的Node.js包再给浏览器用,就需要把基于Node.js的源代码编译成浏览器支持的JS代码。
在本文叙述中,假定:
- 主语言: 最终的主程序所用的语言
- 副语言: 不是主语言的另一种语言
例如,python调用js,python就是主语言,js是副语言
TL; DR
适用于:
- 希望自己的项目不要依赖副语言的runtime。换句话说,python调javascript的时候想只装一个python,javascript调用python的时候也不想装python。
- 副语言基本是纯代码,没有用其他底层实现,也没有引用太多复杂的包。比如,javascript调python,python用了numpy,打咩;python调用javascript,javascript用了一些C++的Node插件,打咩。
- 主语言和副语言交互很频繁,且交互的对象很难序列化(比如函数,复杂的类之类的)。
- 如果是python调用javascript,对运行效率不要有太多要求。
有库!有库!有库!
python调javascript
- Js2Py:
pip install js2py
- 优点:
- 目前为止最好的JavaScript to python翻译器了,除了依赖底层C++实现的包的Node.js包,应该基本都可以翻译,翻译得还蛮快。
- 有翻译成python的中间结果,翻译完之后以后再import同一个javascript包的话,会直接调用python版本。
- 看起来作者最近还在维护。
- 缺点:
- Javascript部分代码也比较复杂的时候,会比直接在Node.js里运行这部分js code慢很多。虽然这个库里也实现了一个python的javascript解释器,能比翻译器快,但目前它实现的版本里只支持运行一段给定的javascript代码,两种语言的交互部分的优势就没了,期待作者后面加更多新feature吧。
- 把它放到自己的应用场景的时候,总有一些小问题,可能需要稍微修改源码优化。比如:
- 对Node.js包的安装-翻译-调用这部分的pipeline做得不是特别好,影响程序效率。比如说,它翻译之前要先把基于ES6的包用
babel
转成ES5,但像babel
这样的依赖项被安装在了临时目录里,导致每次打开python进程执行翻译的时候都要安装一次。不过,同一个python进程里翻译多次不会重复安装,多个python进程里多次引用同一个包也不会,后面几次直接会导入翻译好的python版本。 console.log(a, b, c)
翻译之后只会打印a
,可能需要改一下js2py
里console的实现。
- 对Node.js包的安装-翻译-调用这部分的pipeline做得不是特别好,影响程序效率。比如说,它翻译之前要先把基于ES6的包用
- 优点:
javascript调python
- Brython:是python3的javascript解释器,主要目的是让浏览器可以跑python,没太用过。
- PScript: 把python代码翻译成javascript代码,但正如其包名所说,只能翻译纯python的简单脚本(换句话说,只能翻译python的一个子集)。
- Transcrypt:python to javascript翻译器,也只能翻译一个子集。
- 优点: 功能比较全的javascript to python翻译器。
- 虽然只能翻译python语言的一个子集,但功能比
PScript
还是要全不少的。 - 翻译的时候还有许多宏可以决定要如何翻译。
- 虽然只能翻译python语言的一个子集,但功能比
- 缺点:在我删光了我的python代码里对
numpy
的依赖后,发现collections
包也翻译不了,遂放弃此路。
- 优点: 功能比较全的javascript to python翻译器。
原理
由于python和javascript都不是强类型的语言,因此一定程度上也是可以互译的。其次,由于它们都属于解释型语言,因此也可以用主语言实现一个副语言的解释器,最后统一在主语言的runtime下运行。更深入的原理涉及一些编译原理的知识,咱也不太懂,就只按自己的粗浅理解简单讲一下,不详细展开了,有不对的地方欢迎指正。
这种打不过就加入(……)的方式有两种实现方法,一种是直接翻译成主语言,另一种是用主语言写一个副语言的解释器。翻译和解释的区别在哪儿呢?简单举个例子,假如你是个中文母语者:
- 翻译: 有人跟你说“Go and fetch a book”(副语言),你先在脑中翻译成中文(主语言):“去拿本书”,哦……懂了(解析主语言),然后去拿书了(在主语言runtime下执行指令)
- 解释: 有人跟你说“Go and fetch a book”(副语言),你直接懂了(解析副语言),然后就去拿书了(在主语言runtime下运行)。(这不是我们学外语的理想状态嘛!)
顺便一提,在这个例子里,编译型语言就像是,有人早上跟你说“Go and fetch a book”,经过漫长的训练(编译和链接),然后你就变成了一个只会拿书的机器(可执行文件),这一天里别人只要叫你(运行Run),你就会自动去拿书,不需要再跟你说“Go and fetch a book”(源代码)。
解析副语言
不管是翻译还是解释,首先的第一步应该都是要解析副语言。一般情况下可以选择解析出副语言的抽象语法树(AST),然后根据抽象语法树决定如何翻译,或者如何执行程序。
以副语言为js,即python调用javascript为例。
题外话: javascript有个专门的包esprima.js专门解析js code的AST。Js2Py的作者把它人工翻译成了Python(强!):pyjsparser,成功为js翻译成python打好了基石。
这是pyjsparser
解析出来的一个AST:
>>> from pyjsparser import parse
>>> parse('const abc = "Hello!"\nconst c = abc')
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "abc"
},
"init": {
"type": "Literal",
"value": "Hello!",
"raw": "\"Hello!\""
}
}
],
"kind": "const"
},
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "c"
},
"init": {
"type": "Identifier",
"name": "abc"
}
}
],
"kind": "const"
}
]
}
我们就可以遍历这个AST,然后智能地翻译出等价的python程序。
比如第一层是程序层,body
只有一个元素,说明程序只有两行指令,都是定义变量("VariableDeclaration"
)。
那么怎么定义的呢?从declarations
可知,每个指令只定义了一个变量,且类型都是const
常量(说明后面不允许改变值)。
我们知道定义变量需要知道:(1)变量的名字(2)有无初始值。
第一个变量的名字(id
)是abc
,有初始值(init
),是一个字面量(Literal
),转成python的字面量之后值在value
字段里,声明时的原始代码在raw
里。举个例子,可以简单地翻译为abc = Constant("Hello")
,其中Constant
是特地为JS定义的常量类型。第二个变量的名字(id
)是c
,有初始值(init
),是通过变量定义的,变量的标识符(Identifier
)为abc
。同理,也可以翻译为c = Constant(abc)
。
如果是解释器的话,最后python中只要有2个常量,名字分别是abc
和c
,且值都为"Hello"
就行,不需要特地关心如何得到这个结果的,甚至于可能,我也不需要实际执行两次赋值。
当然实际使用的时候,翻译和解释的实现可能比这复杂得多,但基本都有前人已经做过相关的工作了。
优点
- 只需要装主语言的runtime。对于想把自己的项目作为主语言的插件分发,不要求目标机器一定得有副语言runtime的情况比较适用。举例子的话,比如想直接在线尝试python,可以用python到js的翻译库或者解释器,这样可以不需要装python也可以运行python,对于把python嵌入浏览器可能有不错的效果。
- 无缝交互!因为最后都在一个体系里了,再也不用为了传递函数和复杂类对象而烦恼。
缺点
- 副语言能支持的包有限。比如python转成js的时候,甚至很多原生包(如
collections
都用不了,更别说numpy
之类的了);js转python还好,大概只要不是底层由C++实现的Node.js包的话,应该都可以翻译成功。这基本是通过这种原理javascript调用python的瓶颈。 - 两种语言运行效率不同。由于Javascript运行速度比python快多了,如果是python调用javascript,在整个项目javascript比重很大,且运行效率又很重要的时候,会慢到不能接受(……),个人觉得这是python通过该原理调用javascript的最大瓶颈。