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

适用于:

  1. 希望自己的项目不要依赖副语言的runtime。换句话说,python调javascript的时候想只装一个python,javascript调用python的时候也不想装python。
  2. 副语言基本是纯代码,没有用其他底层实现,也没有引用太多复杂的包。比如,javascript调python,python用了numpy,打咩;python调用javascript,javascript用了一些C++的Node插件,打咩。
  3. 主语言和副语言交互很频繁,且交互的对象很难序列化(比如函数,复杂的类之类的)。
  4. 如果是python调用javascript,对运行效率不要有太多要求。

有库!有库!有库!

python调javascript

  • Js2Pypip install js2py
    • 优点:
      1. 目前为止最好的JavaScript to python翻译器了,除了依赖底层C++实现的包的Node.js包,应该基本都可以翻译,翻译得还蛮快。
      2. 有翻译成python的中间结果,翻译完之后以后再import同一个javascript包的话,会直接调用python版本。
      3. 看起来作者最近还在维护。
    • 缺点
      1. Javascript部分代码也比较复杂的时候,会比直接在Node.js里运行这部分js code慢很多。虽然这个库里也实现了一个python的javascript解释器,能比翻译器快,但目前它实现的版本里只支持运行一段给定的javascript代码,两种语言的交互部分的优势就没了,期待作者后面加更多新feature吧。
      2. 把它放到自己的应用场景的时候,总有一些小问题,可能需要稍微修改源码优化。比如:
        • 对Node.js包的安装-翻译-调用这部分的pipeline做得不是特别好,影响程序效率。比如说,它翻译之前要先把基于ES6的包用babel转成ES5,但像babel这样的依赖项被安装在了临时目录里,导致每次打开python进程执行翻译的时候都要安装一次。不过,同一个python进程里翻译多次不会重复安装,多个python进程里多次引用同一个包也不会,后面几次直接会导入翻译好的python版本。
        • console.log(a, b, c)翻译之后只会打印a,可能需要改一下js2py里console的实现。

javascript调python

  • Brython:是python3的javascript解释器,主要目的是让浏览器可以跑python,没太用过。
  • PScript: 把python代码翻译成javascript代码,但正如其包名所说,只能翻译纯python的简单脚本(换句话说,只能翻译python的一个子集)。
  • Transcrypt:python to javascript翻译器,也只能翻译一个子集。
    • 优点: 功能比较全的javascript to python翻译器。
      1. 虽然只能翻译python语言的一个子集,但功能比PScript还是要全不少的。
      2. 翻译的时候还有许多宏可以决定要如何翻译。
    • 缺点:在我删光了我的python代码里对numpy的依赖后,发现collections包也翻译不了,遂放弃此路。

原理

由于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个常量,名字分别是abcc,且值都为"Hello"就行,不需要特地关心如何得到这个结果的,甚至于可能,我也不需要实际执行两次赋值。

当然实际使用的时候,翻译和解释的实现可能比这复杂得多,但基本都有前人已经做过相关的工作了。

优点

  1. 只需要装主语言的runtime。对于想把自己的项目作为主语言的插件分发,不要求目标机器一定得有副语言runtime的情况比较适用。举例子的话,比如想直接在线尝试python,可以用python到js的翻译库或者解释器,这样可以不需要装python也可以运行python,对于把python嵌入浏览器可能有不错的效果。
  2. 无缝交互!因为最后都在一个体系里了,再也不用为了传递函数和复杂类对象而烦恼。

缺点

  1. 副语言能支持的包有限。比如python转成js的时候,甚至很多原生包(如collections都用不了,更别说numpy之类的了);js转python还好,大概只要不是底层由C++实现的Node.js包的话,应该都可以翻译成功。这基本是通过这种原理javascript调用python的瓶颈
  2. 两种语言运行效率不同。由于Javascript运行速度比python快多了,如果是python调用javascript,在整个项目javascript比重很大,且运行效率又很重要的时候,会慢到不能接受(……),个人觉得这是python通过该原理调用javascript的最大瓶颈
posted @ 2022-01-15 16:57  milliele  阅读(653)  评论(0编辑  收藏  举报