Boa 是如何打通 Python 与 Node.js 世界的?

avatar
阿里巴巴 前端委员会智能化小组 @阿里巴巴

文/Yorkie

今天,给大家介绍一个 Pipcook 背后的技术—— Boa。通过它,开发者可以调用到任何 Python 的函数,当然就包括:numpy、scikit-learn、jieba 等大家熟知的 Python 库,如果想直接使用 tensorflow、pytorch、tvm 等也不在话下。

先来看一个例子:

const boa = require('@pipcook/boa');
const torchtext = boa.import('torchtext');
const { get_tokenizer } = torchtext.data.utils;
const tokenizer = get_tokenizer('basic_english');
const str = 'MEMPHIS, Tenn. – Four days ago, Jon Rahm was \
    enduring the season’s worst weather conditions on Sunday at The \
    Open on his way to a closing 75 at Royal Portrush, which \
    considering the wind and the rain was a respectable showing. \
    Thursday’s first round at the WGC-FedEx St. Jude Invitational \
    was another story. With temperatures in the mid-80s and hardly any \
    wind, the Spaniard was 13 strokes better in a flawless round. \
    Thanks to his best putting performance on the PGA Tour, Rahm \
    finished with an 8-under 62 for a three-stroke lead, which \
    was even more impressive considering he’d never played the \
    front nine at TPC Southwind.';
console.log(tokenizer(str));

上面的代码其实理解起来很简单,通过 boa.import 来加载 Python 模块 torchtext,然后获取到 torchtext.d.utils 的 get_tokenizer 方法,接着就是将一串字符串传入这个方法中,并获得结果。

尽管 tokenizer 内部的实现非常简单,但通过这个示例,大概能知道 Boa 要做的事了,那就是让你能够无缝地使用 Python 生态中的库,我们再来看一个稍微复杂的示例:

const boa = require('@pipcook/boa');
const torch = boa.import('torch');
const torchtext = boa.import('torchtext');
const { nn, optim } = torch;
const { DataLoader, dataset } = torch.utils.data;
const { text_classification } = torchtext.datasets;
const [train_dataset] = text_classification.DATASETS['AG_NEWS'](boa.kwargs({
  root: './.data',
  ngrams: 2,
  vocab: null,
}));
class TextSentiment extends nn.Module {
  constructor(sizeOfVocab, dimOfEmbed, numOfClass) {
    super();
    this.embedding = nn.EmbeddingBag(sizeOfVocab, dimOfEmbed, boa.kwargs({
      sparse: true,
    }));
    this.fc = nn.Linear(dimOfEmbed, numOfClass);
    this.init_weights();
  }
  init_weights() {
    const initrange = 0.5
    this.embedding.weight.data.uniform_(-initrange, initrange);
    this.fc.weight.data.uniform_(-initrange, initrange);
    this.fc.bias.data.zero_();
  }
  forward(text, offsets) {
    const embedded = this.embedding(text, offsets);
    return this.fc(embedded);
  }
}

我们通过这个例子可以看到,通过 Boa 我们 import 了 PyTorch 的模块,并使用了它在 Python 中的 ,来定义一个 TextSentiment 类,它继承自 PyTorch 中的 nn.Module。

好了,关于 Boa 更完整的用法和文档,大家可以去这里:alibaba.github.io/pipcook/#/t…


接下来,我们进入正题,来看看它背后的故事。

首先,我们要想从 Node.js 中调用到 Python,就需要知道如何将 Python 嵌入到其他运行环境,这里大家可以看一下 Python 的官方文档:docs.python.org/3/extending…

看文档的过程中,大家可能会看到一个名词:CPython,这里先为大家稍微解释解释,Python 是一种编程语言规范,而 CPython 则是官方提供的一种 Python 实现,它基于 C 实现,并且可以方便地嵌入到其他系统中。

正如上面的文档所说的,CPython 提供了丰富的操作 Python 的接口,比如:初始化、加载模块、创建对象等,大家其实可以把 CPython 看作是一个对象数据库,我们通过这些接口就能对 Python 世界(CPython)中的各种对象进行各种操作(读和写),因此最后我们要做的就是使用 N-API 和 CPython API,来把 JavaScript 中的各种函数映射到对 Python 世界中对象的操作。

比如,当我们调用 boa.import 的时候,其实是在 Node.js 的 v8 的环境中使用 CPython 的 PyImport_Import(name),当我们在调用一个 boa.import 返回的函数时,其实是调用了对应的 PyObject_Call()。感兴趣的同学可以看这里:docs.python.org/3/c-api/ind…,了解 CPython 到底提供了哪些接口。

以上,基本上解决了如何从 Node.js 调用到 Python 的问题,但是仍然有一些问题需要解决,那就是如何让我们调用 Python 函数、获取对象成员、数组的时候,看起来就像使用 JavaScript 中的函数、对象和数组一样,否则我们的代码可能看起来就是:

const torch = boa.import('torch');
const DataLoader = torch.get('utils').get('data').get('DataLoader');

一个简单的对象访问就要手写 N 遍 get,另一个例子如下:

const { len, tuple } = boa.builtins();
len.call([]);
tuple.call([]);

通过上面的例子又可以看到,我们在调用 Python 函数的时候,又不得不每次都要使用 .call 来显式调用。那么在 Boa 中,上述的例子是怎么表达的呢?

const torch = boa.import('torch');
const { DataLoader } = torch.utils.data;
const { len, tuple } = boa.builtins();
len([]);
tuple([]);

可以看到,Boa 中不仅可以使用 JavaScript 的方式来进行对象访问,还能使用 ES6 Destructor 来解构对象,简化我们的写法,而在函数调用方面,也跟我们平时调用 JavaScript 的方式没有任何区别,这样看上去就大大地降低了使用门槛。那么,这究竟是如何做到的呢?

这里我们依赖于 ES6 Proxy 特性,它允许我们创建一个新的对象,然后我们可以监听这个对象上的一些事件,比如对象访问、函数调用、new 关键字等,然后在这些事件中,再使用 C++ Addon 暴露的方法去操作 Python 中的对象,对这部分实现感兴趣的同学,可以看我们的源码了解:github.com/alibaba/pip…

好了,有了以上这两个能力后,这便成为了 Boa 的“雏形”了,为什么说是雏形呢?因为其实  Boa 仍然还有很长的路要走,比如对于 Python 中的 with 语法,现在仍然需要显示地声明 boa.with() 来使用,对于 Python 中的一些表达式,也只能通过 boa.eval() 来调用。

另一方面,在 Boa 中始终存在两个虚拟机,这样造成的一个最大的问题就是双倍的 GC,如何通过更好的方式,打通两个世界的对象树,从而能高效地管理整个运行时,也是 Boa 之后重要的课题之一。

最后,希望看到这里,仍然有兴趣来想参与 Boa 或 Pipcook 的人,就不要犹豫,欢迎来我们 GitHub 贡献你的想法和代码吧!