在浏览器中用Python做数据科学:Pyodide

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

Pyodide 是Mozilla的一个实验性项目,用于创建一个完全在浏览器中运行的完整的Python数据科学技术栈。

Pyodide的动机来自于Mozilla的另一个项目 Iodide的工作,在早先的一个博客中我们介绍了它。Iodide是一个基于最新Web技术的用于数据科学实验和交流的工具。值得注意的是,它被设计来在浏览器中而不是在远程内核中执行数据科学计算。

不幸的是,我们在浏览器中所用的语言,JavaScript,没有一个成熟的数据科学库。并且它缺失了一些对于数值计算很有用的特性,比如操作符重载。我们仍然认为致力于改变改状况并将JavaScript数据科学生态向前推进是值得的。同时,我们也在选取一个捷径: 我们通过将流行的成熟的Python科学技术栈带入浏览器领域来满足数据科学家。

广泛被争论的就是Python不在浏览器中运行是对该语言的存在的威胁—现在如此多用户交互发生在Web或者移动设备上,它需要也能在上面工作否则就会落后。因此,尽管Pyodide试图要首先满足Iodide,它仍然被设计为也可作为独立体使用

Pyodide带给你一个完全运行在浏览器中的完整的,标准的Python解释器,并具有完整的访问浏览器的Web APIs的能力。在上面的例子中(50MB下载),对加利福利亚奥克兰城当地信息服务中心“311”的访问密度进行了3D作图。数据加载和处理在Python中执行,然后将其递交给Javascript和WebGL来绘图。

另外一个快速示例是,下面有一个简单的小脚本可以让你在浏览器窗口中进行绘画。

image.png

下面就是它看起来的样子:

最好的了解Pyodide可以做什么的方式就是直接去尝试它!这里有一个演示notebook (50MB 下载),其概括了一些高级特征。这篇博客的剩余部分将更多的是对Pyodide如何工作的深入的技术研究。

现有技术

当我们启动Pyodide项目的时候,已经有一些令人印象深刻的项目将Python带到浏览器的世界。不幸的是,还没有任何一个项目致力于实现具有完整功能特性的主流数据科学技术栈(支持 NumPy, Pandas, Scipy, 和Matplotlib),而那正是我们的目标。

诸如 Transcrypt这样的项目将Python转换为JavaScript。由于转换编译这个步骤发生在Python上, 你要么需要提前完成所有的转译工作要么需要与一个服务器交互来完成该工作。这不能真正地满足我们让用户能够在没有外界帮助的情况下在浏览器中编写Python并运行的目标。

BrythonSkulpt 这样的项目是用JavaScript重写了标准的Python解释器,因此,你能够直接在浏览器中运行Python代码。不幸的是,由于它们是由JavaScript启动的完全的新的Python的实现,其不能够与用C写的Python扩展兼容,比如 NumPyPandas。因此,没有数据科学工具。

PyPyJs是在浏览器中JIT编译Python的实现 PyPy的一个可替代品,其使用emscripten。它有潜力能够非常迅速地运行Python代码,原因和PyPy一样。不幸的是,它也像PyPy一样有无法执行C扩展 的问题。所有这些方式使得我们要去重写科学计算工具来获得足够的性能。作为一个经常使用Matplotlib的人,我知道那将占据多少数不清的个人时间:其它的项目已经进行过尝试并停滞不前,而且肯定是会带来许多我们这样的杂凑的创业团队无法处理的工作量。我们因此需要构建一个尽可能地接近标准Python实现的工具,以及大多数据科学家已经使用的科学计算技术栈。

在与Mozilla的WebAssembly 团队交谈之后,我们发现构建的关键是 emscriptenWebAssembly: 用于将向浏览器传送C代码的技术。这导致我们发现了一个已存的但是沉寂的用于emscripten的Python构建, cpython-emscripten,其最终被用作Pyodide的基础。

emscripten与WebAssembly

有许多方式来描述 emscripten为何物,但是对于我们来说最重要的是它提供了两样东西:

  • 一个将C/C++编译到WebAssembly的编译器

  • 一个使浏览器感觉就像是一个原生的计算环境的兼容层

WebAssembly 是一个新的运行于现代Web浏览器中的语言,其是对JavaScript的补充。它是一个类似于汇编语言的运行时几乎具有原生性能被设计作为如C和C++这样的低级语言的编译目标的低级语言。值得注意的是,Python的最流行的解释器CPython就是用C实现的,因此这就是emscripten被创造出来的意义。

Pyodide通过以下过程组合:

  • 从主流的Python 解释器(CPython)下载源码,和科学计算包(NumPy等)

  • 引入很小的改变来使得它们能够在新环境中工作

  • 使用emscripten的编译器将它们编译为WebAssembly

如果你直接将WebAssembly加载进入浏览器,你会发现事情与你直接在你的操作系统上运行Python解释器有非常明显的不同。例如,Web浏览器没有一个文件系统(用于加载和存储文件的地方)。幸运的是,emscripten提供了一个虚拟的文件系统,其使用JavaScript编写并能够被Python解释器使用。这些虚拟的“文件”默认保存在浏览器标签对应的内存上,当你导航离开该页面时,它们就会消失。(emscripten也提供了让该文件系统能够将数据存储在浏览器的持续性本地存储上的方式,但是Pyodide没有使用该功能。)

通过模拟出标准计算环境的文件系统以及其它一些特性,emscripten使得将现存的项目转移到Web浏览器上成为可能而且令人惊喜的是只需要少量的修改。(某一天,我们也许会转移到使用 WASI来作为系统模拟层,但是目前来说emscripten是一个更成熟和完整的选择)。

总的来说,为了能够在你的浏览器中加载Pyodide,你需要下载:

  • 已经编译到WebAssembly的Python解释器

  • 一系列由emscripten提供的JavaScript用于提供系统模拟

  • 一个打包好含有Python解释器所需要的所有的文件的文件系统,最重要的就是Python标准库

这些文件可能会非常大: Python本身就是21MB,NumPy是7MB等等。幸运的是,这些包只需要被下载一次,之后他们会被存储在浏览器的缓存中。

先后完成这些步骤以后,Python解释器就能够访问它的标准库中的文件,启动,以及开始运行用户代码。

哪些能工作,哪些又不能

我们运行了CPython的单元测试作为Pyodide的持续性测试的一部分来理解哪些Python的特性可以工作哪些又不可以工作。一些功能,比如线程现在还不能工作,但是通过最新可获取的WebAssembly 线程,我们应该在不远的将来就能加入支持。

其它特性,例如 底层网络sockets由于浏览器的安全沙盒几乎不可能实现。很抱歉需要告诉你,你希望在你的Web浏览器中运行一个Pythonminecraft 服务器的希望还有很长一段路要走。然而,你可以通过浏览器的API来通过网络获取数据(更多细节在后面的内容)。

它有多快?

在JavaScript虚拟机中运行Python解释器具有性能损失,但是该损失实际上是令人吃惊的小—在我们的基准测试中,相比于原生的速度在Firefox上要慢1到12倍在Chrome上要慢1到16倍。经验表明,这对于交互式开发来说已经足够使用了。

值得注意的是,在Python中有大量的内部循环的代码的速度趋向于以更大的系数慢于那些依赖于NumPy进行内部循环的代码。下面是在同一个硬件上与原生运行相比,在Firefox和Chrome上运行各种纯Python和Numpy基准测试的结果。

在Python和JavaScript之间进行交互

如果所有Pyodide能做的就只是运行Python代码并写出到标准输出上,它将会增长成为一个不错的很酷的技巧,但是不会成为一个用于实际工作的实用工具。真正的力量源于它与浏览器API以及其它运行在浏览器中的JavaScript库交互的能力。由于我们已经将Python解释器编译为了WebAssembly,它也与JavaScript端具有深度的交互。

Pyodide会在许多Python与JavaScript之间的内建数据类型之间进行隐式转换。其中一些转换时很直接明显的,但如往常一样,那就是很有趣的极端情况。

Python将字典和对象实例作为不同的类型来对待。字典是从键到值得映射。另一方面,对象通常拥有一些能够作用于该对象的方法。在JavaScript中,这两个概念被合并到一个单一类型对象。(是的,这里我进行了过度简化来使描述易于理解)

没有对开发者使用JavaScript对象的意图进行真正的理解,要去有效的区分是否应该将其转换为Python字典或者对象将会是不可能的。因此,我们不得不使用一个代理和让”鸭子类型“来处理这个情形。

代理是一个围绕在其它语言中的一个变量的包装器。相比于简单地在JavaScript中读取变量并重写为Python的数据结构(就如基本类型的做法一样),代理坚持了原始的JavaScript变量,并在需要时对它调用方法。这意味着任何JavaScript变量,不管有多么定制化,都是完全能够从Python访问的。代理在其它方面也能工作。

鸭子类型是一个原则,而不是询问一个变量“你是一只鸭子吗?”你问它“你走路像鸭子吗?”以及“你叫得像鸭子吗?”并从中推断它很有可能是一只鸭子,或者至少会像鸭子一样工作。这允许Pyodide来延迟做出如何转换JavaScript对象的决定:它将其包装在一个代理中并让使用它的代码来决定如何处理。当然,这并非总是能工作,鸭子可能实际上是一只兔子。因此,Pyodide也提供了方法来显式地处理这些转换

正是这样级别的紧密集成允许了用户能够在Python中处理它们的数据,并将其发送给JavaScript来可视化。例如,在我们的Hipster Band Finder 展示中,我们表达了如何在Python的Pandas中加载和分析数据,然后将其发送给JavaScript的Plotly 来进行可视化。

访问Web API和DOM

代理也被证明是访问Web API以及浏览器所提供的使API工作的函数集的关键。例如,很大部分Web API是在document对象上。你可以通过如下方式从Python中获取:

image.png

这会将document对象作为一个代理从JavaScript端导入到Python端。你可以开始从Python中对其调用方法:

image.png

所有这些都通过能够动态查询document对象的代理来完成。Pyodide不需要包含一个浏览器所有的全部API的完整列表。

当然,直接使用Web API并不总是最Python化或者用户友好型的工作方式。更棒的方式要等到对Web API的用户友好型的Python包装器的出现,就像jQuery和其它的库如何使得Web API能够从JavaScript中使用一样。 如果你感兴趣于为这个想法来工作,请告知我们

多维数组

有些数据类型是特定对数据科学有用的,而Pyodide对它们也有专门的支持。多维数组是具有相同类型的值(通常是数值)的集合。它们倾向于非常大,并且知道每个元素都是同样的类型相对于能够存储任何类型的Python list和JavaScriptArrays来说具有实际上的性能优势。

在Python中, NumPy 数组是最常用的多维数组的实现。JavaScript具有TypedArrays,其仅含有一个单一的数值类型,但是是一维的,因此需要在其之上构建多维索引。

由于实际上这些数组可能会非常大,我们不想在语言运行时间拷贝它们。那不仅仅会花相当长的时间,而且在内存中同时保留两个拷贝将会加重浏览器所具有的被限制的内存的负担。

幸运的是,我们可以不用拷贝来共享数据。多维数组通常是用少量用于描述值类型和数组形状及内存分布的元数据来实现的。数据本身是从元数据中通过指针访问的另一个内存区域。该内存处于一个叫作“WebAssembly堆”的区域,这带来一个优势,因为其可以从JavaScript和Python中同时访问。我们可以简单地在语言之间拷贝元数据(其本身非常小),并保持指针指向WebAssembly堆中的数据。

这个想法目前对一维数组进行了实现,同时对于高维数组目前工作还处于次最优状态。我们需要对JavaScript端不断改善来获得一个可工作的有用的对象。迄今为止还没有一个用于JavaScript多维数组的很显然的选择。有前景的项目,比如Apache Arrow](https://arrow.apache.org/) 和xnd’s ndarray就正是致力于解决该问题,旨在使语言间运行时的内存结构化数据的传递更容易。对这些项目的构建的调查仍在进行当中以使得这类型的数据转换更为强大。

实时交互可视化

在浏览器中进行数据科学计算相比于如Jupyter一样在远程内核中进行计算的一大优势就是,交互式可视化不用通过网络来传输数据并重新处理和展示这些数据。这很大程度地减少了延迟—用户移动鼠标的时刻与屏幕更新并显示图案的时刻之间的间隔时间。

要使得其能工作需要上面描述到的所有的技术片段能够很好地协同工作。我们使用matplotlib来看一下用于展示正态分布如何工作的交互性示例。首先,通过Python的Numpy产生随机数据。接下来,Matplotlib接管该数据,并使用内建的软件渲染器来将其绘出。它使用Pyodide对零拷贝共享数组的支持来将像素回馈给JavaScript端,在这里数据最终被渲染为HTML的画布。然后浏览器接管工作,将像素显示到屏幕上。用来支持交互性操作的鼠标和键盘事件通过从Web浏览器到Python的回调函数的调用来处理。

软件包

Python科学计算栈并不是单独的一个庞然大物—它实际上是一个一系列相互之间松关联的包在一起工作构造的环境。其中最流行的是用于数值数组和基本计算的 NumPy,用于更复杂和通用计算如线性代数的 Scipy ,用于数据可视化的Matplotlib,用于表格数据或者数据帧的的Pandas 。你可以在 这里看到Pyodide为浏览器构建的完整的持续更新的包列表。

其中一些包是很容易直接被引入Pyodide的。通常来说,任何用纯Python写而没有使用编译语言扩展的部分都是很容易。而稍微有点儿困难的类型是像Matplotlib这样的库,其需要特定的代码来在HTML画布中展示绘图。属于极其困难的一端是Scipy库,它一直是一个相当大的挑战。

Roman Yurchak致力于使Scipy中大量的古老的Fortran被编译到WebAssembly。Kirill Smelkov改善了emscripten以使得共享对象能够被其它共享的对象使用,将Scipy控制到一个可管理的大小。(这些外部贡献者的工作由 Nexedi提供支持)。如果你在将一个包转为Pyodide的时候很费劲儿,可以在GitHub上联系我们: 很有可能我们已经之前就遇到过你的问题。

由于我们不能提前预测用户最终需要用哪些库,它们被按需单独地下载到浏览器中。例如,当你导入NumPy时:image.png

Pyodide抓取NumPy库(以及其所有的依赖)并将它们载入到浏览器。再一次,这些文件只需要被下载一次,然后就被存储在浏览器缓存中。

将新的包添加到Pyodide目前是一个半手动的过程,涉及到添加文件到Pyodide构建中。我们长期以来更希望采用分发的方式来实现该过程,为了任何人都可以在不用遍历单个项目的情况下能贡献包到生态中。最棒的例子就是conda-forge。能将它们的工具扩展来支持WebAssembly作为一个平台将会非常棒,而不是花费大力气重新打造一个。

此外,Pyodide将会很快支持 直接从PyPI(Python的主要社区包仓库)中加载包,如果该包是纯Python编写而且以wheel格式发布。

除了Python以外

Pyodide相对早的成功已经启发了其它语言社区的开发人员,包括Julia,R,OCamlLua,来使这些语言的运行时能够在浏览器中很好地工作,并与如Iodide样的web优先的工具相集成。我们已经定义了一系列的层级来鼓励实现者创造与JavaScript运行时更紧密的集成。

  • 层级 1:仅包含字符串输出,为了能够对于基本操纵台REPL(read-eval-print-loop)有用

  • 层级 2:转换基本数据类型(数值,字符串,数组和对象)到或从JavaScript

  • 层级 3:在客户端语言与JavaScript之间共享类实例(有方法的对象)。这允许访问Web API

  • 层级 4:在客户端语言和JavaScript之间共享数据科学相关类型(n维数组和数据帧)

我们肯定想要鼓励这个勇敢的新世界,并对能够有可能让更多的语言之间相互操作而感到兴奋。让我们知道你在做哪方面的努力!

结语

如果你还没有在实践中尝试Pyodide,快去试一下吧(50MB下载)

能够看见所有那些在Pyodide发布后段时间内就使用其创建的很酷的东西是非常让人欣慰的。然而,仍然还有很多工作需要完成来将这个概念验证性的实验转换为一个用于日常数据科学工作的专业性的工具。如果你感兴趣于帮助我们构建未来,赶快来gitter, github邮件列表找到我们吧。



英文原文:https://hacks.mozilla.org/2019/04/pyodide-bringing-the-scientific-python-stack-to-the-browser/
译者:青蒿素