Pyodide:将科学 Python 堆栈带到浏览器

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

Density of 311 calls in Oakland, California

Pyodide 的动力来自于另一个 Mozilla 项目,Iodide,我们曾在 早期文章 中介绍过它。Iodide 是一个基于最先进的 Web 技术的用于数据科学实验和交流的工具。值得注意的是,它旨在在浏览器内执行数据科学计算,而不是在远程内核上。

不幸的是,浏览器中我们拥有的“通用语言”JavaScript 没有成熟的数据科学库套件,而且它缺少一些对数值计算有用的特性,例如 运算符重载。我们仍然认为在改变这一点并 推动 JavaScript 数据科学生态系统发展 是值得的。同时,我们也采取了捷径:我们将流行且成熟的 Python 科学堆栈带到浏览器,以满足数据科学家的需求。

人们还普遍认为,Python 在浏览器中无法运行是对该语言的生存威胁——因为如此多的用户交互发生在网络或移动设备上,所以 Python 需要在那里工作,否则就会被淘汰。因此,虽然 Pyodide 首先尝试满足 Iodide 的需求,但它被设计成 本身也有用

Pyodide 提供了一个完整的标准 Python 解释器,它完全在浏览器中运行,可以完全访问浏览器的 Web API。 在上面的示例中(50 MB 下载),加州奥克兰市“311”本地信息服务的呼叫密度被绘制成 3D 图。数据加载和处理是在 Python 中执行的,然后它传递给 Javascript 和 WebGL 进行绘图。

另一个简单的示例,这是一个让你在浏览器窗口中绘画的简单涂鸦脚本


from js import document, iodide

canvas = iodide.output.element('canvas')
canvas.setAttribute('width', 450)
canvas.setAttribute('height', 300)
context = canvas.getContext("2d")
context.strokeStyle = "#df4b26"
context.lineJoin = "round"
context.lineWidth = 5

pen = False
lastPoint = (0, 0)

def onmousemove(e):
    global lastPoint

    if pen:
        newPoint = (e.offsetX, e.offsetY)
        context.beginPath()
        context.moveTo(lastPoint[0], lastPoint[1])
        context.lineTo(newPoint[0], newPoint[1])
        context.closePath()
        context.stroke()
        lastPoint = newPoint

def onmousedown(e):
    global pen, lastPoint
    pen = True
    lastPoint = (e.offsetX, e.offsetY)

def onmouseup(e):
    global pen
    pen = False

canvas.addEventListener('mousemove', onmousemove)
canvas.addEventListener('mousedown', onmousedown)
canvas.addEventListener('mouseup', onmouseup)

它看起来像这样

Interactive doodle example

要了解有关 Pyodide 可以做什么的更多信息,最好的方法是直接尝试它!有一个 演示笔记本(50 MB 下载)介绍了高级功能。本文的其余部分将更深入地探讨它的工作原理。

现有技术

在我们开始 Pyodide 时,已经有一些令人印象深刻的项目将 Python 带到了浏览器。不幸的是,没有一个能满足我们支持完整的全功能主流数据科学堆栈的具体目标,包括 NumPyPandasScipyMatplotlib

Transcrypt 这样的项目将 Python 转换为 JavaScript(转换)。由于转换步骤本身是在 Python 中发生的,因此您要么需要提前完成所有转换,要么需要与服务器通信来完成这项工作。这实际上没有满足我们让用户在浏览器中编写 Python 并无需任何外部帮助即可运行它的目标。

BrythonSkulpt 这样的项目是标准 Python 解释器的重写,它们被重写成 JavaScript,因此它们可以直接在浏览器中运行 Python 代码字符串。不幸的是,由于它们完全是 Python 的新实现,并且是在 JavaScript 中实现的,因此它们与用 C 编写的 Python 扩展(如 NumPyPandas)不兼容。因此,没有数据科学工具。

PyPyJs 是使用 emscripten 将替代的即时编译 Python 实现 PyPy 构建到浏览器中的一个版本。它具有以与 PyPy 相同的原因在浏览器中快速运行 Python 代码的潜力。不幸的是,它具有与 PyPy 相同的 C 扩展性能问题

所有这些方法都要求我们重写科学计算工具以实现足够的性能。作为曾经 在 Matplotlib 上投入大量时间 的人,我知道这需要多少不为人知的工时:其他项目已经 尝试过并停滞不前,这肯定比我们这个简陋的新兴团队能够处理的要多得多。因此,我们需要构建一个尽可能接近标准 Python 实现和大多数数据科学家已经使用的科学堆栈的工具。 

在与一些 Mozilla 的 WebAssembly 大师 讨论后,我们发现构建的关键是 emscriptenWebAssembly:将用 C 编写的现有代码移植到浏览器的技术。这导致发现了 emscripten 的一个现存但休眠的 Python 构建,cpython-emscripten,它最终被用作 Pyodide 的基础。

emscripten 和 WebAssembly

有很多方法可以描述 emscripten 是什么,但最重要的是,它为我们的目的提供了两件事

  1. 一个从 C/C++ 到 WebAssembly 的编译器
  2. 一个使浏览器感觉起来像本地计算环境的兼容层

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 本身是 21 MB,NumPy 是 7 MB,等等。幸运的是,这些包只需要下载一次,之后就会存储在浏览器的缓存中。

通过协同使用所有这些部分,Python 解释器可以访问其标准库中的文件,启动并开始运行用户的代码。

哪些有效,哪些无效

我们在 Pyodide 的持续测试中运行 CPython 的单元测试,以了解 Python 的哪些功能有效,哪些无效。 有些东西,比如 线程,现在还无法正常工作,但随着新推出的 WebAssembly 线程,我们应该能够在不久的将来添加支持。 

其他功能,比如 低级网络套接字,不太可能永远有效,因为浏览器的安全沙箱。 很抱歉要告诉你,你在 Web 浏览器中运行 Python Minecraft 服务器 的希望可能还需要很长时间才能实现。但是,你仍然可以使用浏览器的 API 在网络上获取东西(详情见下文)。

速度有多快?

在 JavaScript 虚拟机中运行 Python 解释器会带来性能损失,但事实证明这种损失非常小——在我们的基准测试中,比 Firefox 上的原生运行速度慢 1 倍到 12 倍,比 Chrome 上的原生运行速度慢 1 倍到 16 倍。经验表明,这对于交互式探索来说非常可用。

值得注意的是,在 Python 中运行大量内循环的代码,其运行速度往往比依赖 NumPy 执行内循环的代码慢得多。以下是使用 Firefox 和 Chrome 运行各种 纯 Python 和 NumPy 基准测试 的结果,与在相同硬件上本地运行的结果相比。

Pyodide benchmark results: Firefox and Chrome vs. native

Python 与 JavaScript 之间的交互

如果 Pyodide 只能够运行 Python 代码并写入标准输出,那么它只是一个很酷的技巧,但对于实际工作来说并不实用。真正的强大之处在于它能够在非常细微的层面上与浏览器 API 和其他 JavaScript 库进行交互。WebAssembly 的设计旨在与浏览器中运行的 JavaScript 轻松交互。由于我们已将 Python 解释器编译为 WebAssembly,因此它也与 JavaScript 端深度集成。

Pyodide 会隐式地在 Python 和 JavaScript 之间转换许多内置数据类型。其中一些转换很直接且显而易见,但一如既往,有趣的总是边缘情况。

Conversion of data types between Python and JavaScript

Python 将 dictobject 实例视为两种不同的类型。dict(字典)只是键值映射。另一方面,object 通常具有“对这些对象执行某些操作”的方法。在 JavaScript 中,这两个概念被合并为一种称为 Object 的类型。(是的,为了说明问题,我在这里进行了过度简化。)

如果不真正了解开发人员对 JavaScript Object 的意图,就无法有效地猜测它应该转换为 Python dict 还是 object。因此,我们必须使用代理,让“鸭子类型”来解决这个问题。

代理是对另一种语言中变量的包装。与简单地读取 JavaScript 中的变量并用 Python 结构对其进行重写一样(如基本类型那样),代理会保留原始的 JavaScript 变量,并“按需”调用其方法。这意味着任何 JavaScript 变量,无论其自定义程度如何,都可以从 Python 中完全访问。代理在另一个方向上也起作用。

鸭子类型是指,与其问一个变量“你是鸭子吗?”,不如问它“你像鸭子一样走路吗?”以及“你像鸭子一样嘎嘎叫吗?”,并由此推断它可能是一只鸭子,或者至少做着像鸭子一样的事情。这使得 Pyodide 能够推迟对如何转换 JavaScript Object 的决定:它将该对象包装在代理中,并让使用它的 Python 代码决定如何处理它。当然,这并不总是奏效,鸭子实际上可能是兔子。因此,Pyodide 还提供了一些方法来 显式地处理这些转换

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

访问 Web API 和 DOM

事实证明,代理也是访问 Web API 的关键,或者说是浏览器提供的一组函数,这些函数使浏览器能够执行某些操作。例如,Web API 的很大一部分位于 document 对象上。你可以通过以下方式从 Python 中获取它:

from js import document

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

document.getElementById("myElement")

所有这些都是通过代理完成的,代理会即时查找 document 对象能够执行的操作。Pyodide 不需要包含浏览器具有的所有 Web API 的全面列表。

当然,直接使用 Web API 并不总是感觉是最 Python 式或最友好的方式。能够看到针对 Web API 的用户友好的 Python 包装器的创建,就像 jQuery 和其他库如何使 Web API 从 JavaScript 中更容易使用一样,将是件很棒的事情。 请告诉我们,如果您有兴趣参与此类工作!

多维数组

有一些重要的数据类型是特定于数据科学的,Pyodide 也对这些类型提供了特殊支持。多维数组是(通常为数字)值的集合,所有值都具有相同的类型。它们往往非常大,并且知道每个元素都是相同类型,这与 Python 的 list 或 JavaScript 的 Array(可以保存任何类型的元素)相比,具有真正的性能优势。

在 Python 中,NumPy 数组 是多维数组最常见的实现。JavaScript 具有 TypedArrays,它只包含单个数字类型,但它们是一维的,因此需要在顶部构建多维索引。

由于在实践中这些数组可能非常大,因此我们不想在语言运行时之间复制它们。这不仅需要很长时间,而且在内存中同时保存两个副本会给浏览器可用的有限内存带来负担。

幸运的是,我们可以共享这些数据而不进行复制。多维数组通常使用少量元数据来实现,元数据描述了值的类型、数组的形状以及内存布局。数据本身通过指向内存中另一个位置的指针从该元数据中引用。这些内存位于称为“WebAssembly 堆”的特殊区域中,这对 JavaScript 和 Python 都是可访问的,这是一个优势。我们只需要在语言之间来回复制元数据(它很小),而将指向数据的指针保留在 WebAssembly 堆中。

Sharing memory for arrays between Python and Javascript

此想法目前已在单维数组中实现,对于更高维数组,则使用了一个次优的变通方法。我们需要改进 JavaScript 端,以便在那里有一个有用的对象可以使用。到目前为止,还没有一个明显的 JavaScript 多维数组选择。诸如 Apache Arrowxnd 的 ndarray 之类的有前途的项目正在处理此问题空间,并旨在使语言运行时之间内存中结构化数据的传递更加容易。正在进行调查以利用这些项目,使这种数据转换功能更强大。

实时交互式可视化

在浏览器中执行数据科学计算而不是在远程内核中执行(如 Jupyter 所做的那样)的优点之一是,交互式可视化不需要通过网络进行通信以重新处理和重新显示其数据。这大大减少了延迟 - 从用户移动鼠标到更新后的绘图显示到屏幕上的往返时间。

要使这成为可能,需要上面描述的所有技术部分协同工作。让我们看看这个 交互式示例,它展示了如何使用 matplotlib 来显示对数正态分布。首先,使用 NumPy 在 Python 中生成随机数据。接下来,Matplotlib 获取这些数据,并使用其内置软件渲染器绘制它。它使用 Pyodide 对零复制数组共享的支持将像素发送回 JavaScript 端,在那里最终将它们渲染到 HTML 画布中。然后,浏览器会处理将这些像素发送到屏幕。用于支持交互性的鼠标和键盘事件由从 Web 浏览器调用回 Python 的回调处理。

Interacting with distributions in matplotlib

打包

Python 科学堆栈不是一个整体——它实际上是一组松散关联的软件包,它们共同协作以创建一个高效的环境。其中最受欢迎的是 NumPy(用于数字数组和基本计算)、Scipy(用于更复杂的通用计算,如线性代数)、Matplotlib(用于可视化)和 Pandas(用于表格数据或“数据帧”)。你可以查看 Pyodide 为浏览器构建的软件包的完整且不断更新的列表 此处

其中一些软件包在 Pyodide 中非常容易集成。通常情况下,任何用纯 Python 编写的没有编译语言扩展的代码都很容易集成。中等难度的是像 Matplotlib 这样的项目,它需要特殊的代码才能在 HTML 画布中显示绘图。在难度最大的方面,Scipy 一直是一个相当大的挑战,并且仍然是如此。

Roman Yurchak 致力于将 Scipy 中大量的遗留 Fortran 编译成 WebAssembly。Kirill Smelkov 改进了 emscripten,使共享对象能够被其他共享对象重用,从而使 Scipy 的大小更加可控。(这些外部贡献者的工作得到了 Nexedi 的支持)。如果你在将软件包移植到 Pyodide 时遇到了困难,请与我们联系 在 Github 上:很可能我们之前已经遇到过你的问题。

由于我们无法预测用户最终需要哪些软件包来完成他们的工作,因此这些软件包会根据需要单独下载到浏览器中。例如,当你导入 NumPy 时:

import numpy as np

Pyodide 会在此时获取 NumPy 库(及其所有依赖项),并将它们加载到浏览器中。同样,这些文件只需要下载一次,并且从那时起就会存储在浏览器的缓存中。

向 Pyodide 添加新软件包目前是一个半手动流程,需要将文件添加到 Pyodide 构建中。从长远来看,我们更倾向于采用一种分布式方法来解决这个问题,这样任何人都可以向生态系统贡献软件包,而无需通过单个项目。最好的例子是 conda-forge。能够扩展其工具以支持 WebAssembly 作为平台目标,而不是重新进行大量工作,将是一件很棒的事情。

此外,Pyodide 将 很快支持 直接从 PyPI(Python 的主要社区软件包存储库)加载软件包,如果该软件包是纯 Python 并且以 wheel 格式 分发其软件包。这使 Pyodide 能够访问大约 59,000 个软件包,截至今天。

超越 Python

Pyodide 的相对早期成功已经激励了来自其他语言社区的开发人员,包括 Julia、R、OCamlLua,使他们的 语言运行时 在浏览器中良好运行,并与 Iodide 等以 Web 为中心的工具集成。我们定义了一组级别来鼓励实施者创建与 JavaScript 运行时更紧密的集成

  • 级别 1:仅字符串输出,因此它可用作基本的控制台 REPL(读取-评估-打印-循环)。
  • 级别 2:将基本数据类型(数字、字符串、数组和对象)转换为 JavaScript,反之亦然。
  • 级别 3:在客
  • 第四级: 在访客语言和 JavaScript 之间共享数据科学相关类型(*n* 维数组和数据帧)。

我们绝对希望鼓励这种充满活力的新世界,并对更多语言相互操作的可能性感到兴奋。请告诉我们您正在进行什么工作!

结论

如果您还没有尝试过 Pyodide,请立即尝试!(下载大小为 50MB)

看到 Pyodide 在公开发布后不久就创建的所有酷炫的东西,我们感到非常欣慰。但是,要将这个实验性的概念验证转化为日常数据科学工作的专业工具,还有很多工作要做。如果您有兴趣帮助我们构建未来,请在gittergithub我们的邮件列表 上找到我们。


衷心感谢Brendan ColloranHamilton UlmerWilliam Lachance 为 Iodide 做出的贡献,并感谢他们审阅了本文,以及Thomas Caswell 提供的额外审阅。

关于 Michael Droettboom

Michael Droettboom 是 Mozilla 的数据工程师,利用数据改进网络,同时尊重用户的隐私。他构建了软件工具来支持许多其他学科,包括计算人文科学、天文学和医学。他是 matplotlib 的前主要开发人员,也是 airspeed velocity 的最初作者。

更多 Michael Droettboom 的文章…


8 条评论

  1. begueradj

    太棒了!

    2019 年 4 月 17 日,凌晨 1:52

  2. Earl Bingham

    我真的很想知道这与使用 R 和 C++ 完成的大量数据科学报告相比如何?

    2019 年 4 月 17 日,上午 11:36

  3. Eric Mandel

    不错!您可能想看看 JS9,它是一个基于 Web 的天体物理学图像显示工具,它也在后台使用 Emscripten:https://js9.si.eduhttps://github.com/ericmandel/js9。Pyodide 和 JS9 的结合开始听起来像 Python(astropy)和 DS9 当前桌面范式的完整基于 Web 的替代方案……

    2019 年 4 月 18 日,上午 10:09

  4. Malith

    所以最终目标是降低使用 Python 进行数据科学探索的入门门槛?

    2019 年 4 月 18 日,上午 10:53

  5. Andre

    Mozilla 做得好!
    这个项目潜力巨大,继续努力!
    谢谢

    2019 年 4 月 19 日,凌晨 2:21

  6. h_o3

    嗨。

    这非常非常有趣。但是我的 iPad/MacBook Pro 的 Safari 和 Chrome 在下载 Python 插件后会停止初始化。

    我知道您担心根据这篇文章在 Iodide 环境中运行 R:http://r.789695.n4.nabble.com/Compile-R-to-WebAssembly-Emscripten-td4755529.html

    2019 年 4 月 19 日,上午 10:40

  7. Alon

    我也对此印象深刻,并尝试在本地使用它。到目前为止,我的两个问题是

    - 导入 pandas 需要很长时间(约 30 秒?)
    - 没有自动完成

    我认为可以将其集成到 jupyter/jupyterlab 中,但我不知道对这两个项目有什么好处。您对此有什么想法吗?

    2019 年 4 月 20 日,晚上 10:26

  8. Stu

    使用它访问基于 C 的库和轮子以及 Brython 作为大多数解释器是否有性能潜力?

    2019 年 4 月 24 日,下午 12:58

本文的评论已关闭。