近日,Mozilla 发布了一个实验项目 Pyodide,旨在浏览器内运行一个完整的 Python 数据科学堆栈。
链接:
https://github.com/iodide-project/pyodide/
Pyodide 的创意起源于 Mozilla 的另一个项目 Iodide,Iodide 是一款基于最先进 Web 技术的数据科学实验和通信工具。值得注意的是,它是设计用于在浏览器中,而不是在远程内核上执行数据科学计算的。
不幸的是,浏览器中“人人都有的语言”Javascript,不仅没有成熟的数据科学库,还缺少许多对数值计算有用的功能,例如运算符重载。一方面我们认为业界应该改变这一现状,并推动 Javascript 数据科学生态系统向前发展;另一方面我们找到了一条捷径:那就是将流行且成熟的 Python 科学栈引入浏览器来满足数据科学家的需求。
此外还有更多人认为,Python 语言面临的一种生存威胁就是无法在浏览器内运行,当下有如此多的用户交互是在网络或移动设备上发生的,Python 要么融入其中,要么就会逐渐落伍。因此,尽管 Pyodide 首先是要满足 Iodide 的需求,但它独立工作时也是由自己用武之地的:
https://github.com/iodide-project/pyodide/blob/master/docs/using_pyodide_from_Javascript.md
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)
它输出成这样:
想要了解 Pyodide 还可以做哪些事情,最好的方法就是去试一试!这里有一个示例笔记(https://alpha.iodide.io/notebooks/300/,50MB)介绍了它的高级功能。下文将深入探讨其工作原理。
在 Pyodide 诞生之前,已经有许多令人印象深刻的项目将 Python 引入浏览器了。然而,包括 NumPy、Pandas、Scipy 和 Matplotlib 在内的这些项目都没有做到实现全功能主流数据科学栈的程度。
像 Transcrypt 这样的项目会将 Python 转换为 Javascript。因为转换步骤是在 Python 中完成的,所以你需要提前做好所有转换,或者连上一台服务器来完成这项工作。这也无法满足我们的需求,也就是让用户在浏览器中编写 Python,并在没有任何外部辅助的情况下运行代码。
像 Brython 和 Skulpt 这样的项目是将标准 Python 解释器重写为 Javascript,因此它们可以直接在浏览器中运行 Python 代码串。但由于它们是 Python 的全新实现,并且需要在 Javascript 中引导,因此它们与用 C 编写的 Python 扩展,例如 NumPy 和 Pandas 等并不兼容,也就没有数据科学工具可用。
PyPyJs 是 Python 的实时编译工具 PyPy 的变体,其使用 emscripten 即时编译 Python 到浏览器上。和 PyPy 一样,它也有能力快速运行 Python 代码。但它也和 PyPy 一样在 C 语言扩展的性能方面存在问题。
上面这些方法都需要我们重写科学计算工具以获得足够的性能。我曾为 Matplotlib 做过很多工作,所以知道重写代码得花费多少劳力:这条路其他项目已经尝试过且举步维艰,而且它要做的工作肯定不是我们这支拼凑起来的新团队能够处理的。因此,我们需要构建一个尽可能基于 Python 的标准实现和多数数据科学家正在使用的科学栈的工具。
与 Mozilla 的几位 WebAssembly 专家讨论之后,我们意识到开发这个工具的关键在于 emscripten 和 WebAssembly:它们是用来将 C 语言编写的现有代码移植到浏览器上的技术。随后我们发现了一个使用 Python 构建的高水平 empscripten 实现,也就是 cpython-emscripten(https://github.com/dgym/cpython-emscripten),最后它成为了 Pyodide 的基础。
可以从很多角度来介绍 emscripten 的内容,但对我们而言最重要的是它的两项用途:
将 C/C++ 编译到 WebAssembly
作为兼容层,在浏览器中模仿原生计算环境
WebAssembly 是一种在现代 Web 浏览器中运行的新语言,是 Javascript 的补充。它是一种类似于群集的底层语言,旨在作为 C 和 C++ 等底层语言的编译目标,提供接近原生环境的性能。值得注意的是,最流行的 Python 解释器 CPython 就是用 C 实现的,所以这里就是 emscripten 的用武之地了。
Pyodide 的工作流程如下:
下载主流 Python 解释器(CPython,https://github.com/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 threads 的帮助下,我们应该很快就能提供支持了。
由于浏览器的安全沙箱限制,一些功能(如底层网络套接字,https://docs.python.org/3/library/socket.html)不太可能正常工作。另外很抱歉让你失望了,想要在 Web 浏览器中运行 Python minecraft 服务器(https://github.com/Yardanico/puremine)可能还有很长的路要走。不过你仍然可以使用浏览器的 API 通过网络获取内容(后文会具体介绍)。
在 Javascript 虚拟机中运行 Python 解释器会损失更多性能,但这种惩罚是很小的——在我们的基准测试中,Firefox 上比原生慢 1 到 12 倍,Chrome 上慢 1 到 16 倍。经验表明,这个程度的性能对于交互探索已经很够用了。
要注意的是,在 Python 中运行大量内部循环的代码往往比使用 NumPy 执行其内部循环的代码慢很多。以下是在 Firefox 和 Chrome 中运行各种 Pure Python 和 Numpy 基准测试(https://github.com/iodide-project/pyodide/tree/master/benchmark/benchmarks)的结果,与在相同硬件上原生运行做对比。
如果 Pyodide 能做的就只是运行 Python 代码并写入标准输出,那么它就只能当一个很酷的玩具,但难以用于实际工作。Pyodide 真正的力量在于它能够以非常高的水平与浏览器 API 和其他 Javascript 库交互。WebAssembly 旨在轻松地与浏览器中运行的 Javascript 交互。由于我们已经将 Python 解释器编译为 WebAssembly,因此它也与 Javascript 这边深度集成在了一起。
Pyodide 会隐式转换 Python 和 Javascript 之间的许多内置数据类型。其中一些转换是明显、直截了的,但有趣的部分向来都在于那些少见的情况。
Python 将 dict 和 object 实例视为两种不同的类型。dict(字典)只是键的映射值。另一方面,object 通常具有对这些对象“做某事”的方法。在 Javascript 中,这两个概念被混合成一个名为 Object 的类型。(是的,我在这里简化了很多内容。)
如果没有真正理解开发者使用 Javascript 的 Object 的意图,就没法判断它是该转换为 Python 的 dict 还是 object。因此,我们必须使用代理,用“鸭子类型”来解决问题。
代理是另一种语言中变量的包装器。代理不是简单地在 Javascript 中读取变量并根据 Python 的构造重写它,就像对基本类型所做的那样,而是维持原始的 Javascript 变量并“按需”调用它上面的方法。这意味着任何 Javascript 变量,无论它的定制程度多高,都可以从 Python 完整访问。反过来也是没问题的。
鸭子类型是一个原则,不是问变量“你是鸭子吗?”而是问它“你像鸭子一样走路吗?”和“你像鸭子一样嘎嘎叫吗?”并从中推断出它可能是一只鸭子, 或者至少像鸭子般行事。这使 Pyodide 可以延后决定如何转换 Javascript 对象:它将对象包装在代理中,并让使用它的 Python 代码决定如何处理它。当然这招并不总能见效,因为你以为它是鸭子,实际上可能是一只兔子(https://www.illusionsindex.org/i/duck-rabbit)。因此 Pyodide 还提供了显式处理这些转换的方法:
https://github.com/iodide-project/pyodide/blob/master/docs/api_reference.md#pyodideas_nested_listobj
正是这种紧密的集成让用户可以在 Python 中处理数据,然后将其发送到 Javascript 进行可视化。例如,在我们的 Hipster Band Finder 演示(https://alpha.iodide.io/notebooks/1623/)中,我们在 Python 的 Pandas 中加载和分析数据集,然后将其发送到 Javascript 的 Plotly 进行可视化。
代理也是访问 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 使用一样。如果你想做这种工作,请告诉我们:
https://gitter.im/iodide-project/iodide
数据科学有自己特定的一些重要数据类型,Pyodide 也对它们提供了专门支持。其中,多维数组是所有相同类型(通常是数字)值的集合。它们往往很大,并且由于每个元素都是相同的类型,多维数组与可以容纳任意类型元素的 Python 的 list 或 Javascript 的 Array 相比,前者具有明显的性能优势。
在 Python 中,NumPy arrays 是多维数组的最流行实现。Javascript 也有 TypedArrays,只包含一个数字类型,但它们是单维的,因此需要在其上构建多维索引。
由于实践中这些数组可能会变得非常大,所以我们不希望在各个语言运行时之间复制它们。这不仅需要很长时间,而且在内存中同时存在两个副本会对浏览器有限的可用内存造成负担。
还好我们无需复制即可共享此数据。多维数组通常使用少量元数据来实现,这些元数据描述了值的类型、数组的形状和内存布局。数据本身通过指向内存中另一个位置的指针从该元数据中引用。这部分内存位于一个称为“WebAssembly 堆”的特殊区域,优点在于它既能用 Javascript 也能用 Python 访问。我们可以简单地在语言之间来回复制元数据(元数据非常小),然后用指针指向引用 WebAssembly 堆的数据。
目前,这个想法是针对一维数组实现的,对于更高维数组而言,这是一个次优的解决方法。我们需要对 Javascript 侧进行改进,以便用一个有用的对象来处理它。现在 Javascript 多维数组还没有很好的选择。Apache Arrow 和 xnd 的 ndarray(https://xnd.io/)等有前景的项目正在研究这方面的问题,旨在使不同语言运行时之间的内存结构化数据的传递更加便利。我们正在研究如何利用这些项目,以进一步增强这种数据转换操作。
如 Jupyter 所做的那样,在浏览器中而不是在远程内核中进行数据科学计算的优点之一是,交互式可视化不必通过网络通信以重新处理和显示其数据。这大大降低了延迟——也就是从用户移动鼠标到更新绘图显示到屏幕所需的响应时间。
完成这项工作需要上述所有技术部分协同工作。先来看看这个交互式示例(https://alpha.iodide.io/notebooks/1658/),它使用 matplotlib 展示了对数正态分布的原理。首先,使用 Numpy 在 Python 中生成随机数据;接下来 Matplotlib 获取该数据,并使用其内置的软件渲染器绘制它;它使用 Pyodide 对零拷贝数组共享的支持将像素发送回 Javascript 端,最终将它们渲染到 HTML 画布中。然后浏览器将这些像素显示屏幕上。用于交互的鼠标和键盘事件由从 Web 浏览器调用的回调处理,发送回 Python。
Python 科学栈不是一个整体——它实际上是一组松散的附属包,它们协同工作以打造生产环境。其中最受欢迎的是 NumPy(用于数值数组和基本计算)、Scipy(用于更复杂的通用计算,如线性代数),Matplotlib(用于可视化)和 Pandas(用于表格数据或“数据帧”)。可以在此处查看 Pyodide 为浏览器构建的完整包列表,该列表还在不断更新:
https://github.com/iodide-project/pyodide/tree/master/packages
其中一些包很容易引入 Pyodide。通常来说,任何使用纯 Python 编写而没有编译语言扩展的东西都非常简单。难度高一些的类别中,像 Matplotlib 这样的项目需要使用特殊代码在 HTML 画布中显示绘图。有些包属于极端困难的类型,其中 Scipy 就一直是一项巨大的挑战。
Roman Yurchak 致力于将 Scipy 中的大量旧 Fortran 代码编译为 WebAssembly。Kirill Smelkov 改进了 emscripten,使共享对象可以被其他共享对象复用,从而缩小了 Scipy 的体积。(这些外部贡献者的工作得到了 Nexedi,http://www.nexedi.com/ 的支持)。如果你正在努力将包移植到 Pyodide 上,请在 Github 上与我们联系,你的问题我们之前很可能也遇到过:
https://github.com/iodide-project/pyodide/
由于我们无法预测用户最终需要哪些包来完成任务,因此用户可以根据需要将它们单独下载到浏览器中。例如在导入 NumPy 时:
import numpy as np
Pyodide 会获取 NumPy 库(及其所有依赖项)并同时将它们加载到浏览器中。同样,这些文件只需要下载一次,之后会存储在浏览器的缓存中。
现在向 Pyodide 添加新包是一个半手动过程,其中需要向 Pyodide 中添加文件。长远来看,我们更倾向于采用分布式方法解决这个问题,这样任何人都可以无需处理单个项目就为生态系统贡献包。这方面最好的例子是 conda-forge。如果能将它们的工具进一步扩展,支持 WebAssembly 作为平台目标就好了,这样就省去了很多重复劳动。
此外,Pyodide 很快将支持(https://github.com/iodide-project/pyodide/pull/147)直接从 PyPI(Python 的主社区包存储库,https://pypi.org/)加载包,只要它以纯 Python 编写并以轮格式(https://pythonwheels.com/)分发。这样以来,Pyodide 现在就可以访问大约 59,000 个包。
Pyodide 较早期的成功已经激励了其他语言,包括 Julia、R、OCaml、Lua 等社区的开发者,设法让他们的语言运行时在浏览器中正常工作,并与 Iodide 等网络优先工具集成。我们定义了一组级别,以鼓励人们创建与 Javascript 运行时联系更紧密的集成:
第 1 级:只是字符串输出,可以用作基本控制台 REPL(read-eval-print-loop)。
第 2 级:将基本数据类型(数字、字符串、数组和对象)与 Javascript 互相转换。
第 3 级:在访客语言和 Javascript 之间共享类实例(带方法的对象)。这样就能允许 Web API 访问了。
第 4 级:在访客语言和 Javascript 之间共享数据科学相关类型(n 维数组和数据帧)。
如果你尚未尝试过 Pyodide,现在就试试吧:
https://alpha.iodide.io/notebooks/300/
https://hacks.mozilla.org/2019/04/pyodide-bringing-the-scientific-python-stack-to-the-browser/
支付宝背后的OceanBase:国产自研分布式数据库
张鑫旭,资深前端工程师,《CSS 世界》作者,工作 10 年一直在专业一线,没有管理任何人,但过得还不错,在圈内也是小有名气。我们来听他聊聊工作 10 年以后,他在前端专业成长路上的探索。更多关于前端圈的最新技术干货欢迎点击“阅读原文”了解,也可咨询小姐姐:18514549229(同微信)。