nbdev:使用 Jupyter Notebooks 完成所有事情

nbdev 是一个 Python 编程环境,它允许您在 Jupyter Notebooks 中创建完整的 Python 包,包括测试和丰富的文档系统。
技术
作者

Jeremy Howard

发布日期

2019 年 12 月 2 日

“我真的认为 [nbdev] 是编程环境向前迈出的巨大一步”:Chris Lattner,Swift、LLVM 和 Swift Playgrounds 的发明者。

我和 fast.ai 的同事 Sylvain Gugger 在过去几年里一直倾注心血开发一个项目。它是一个名为 nbdev 的 Python 编程环境,允许您在 Jupyter Notebooks 中创建完整的 Python 包,包括测试和丰富的文档系统。我们已经使用 nbdev 编写了一个大型编程库 (fastai v2),以及一系列小型项目。

Nbdev 是一个我们称之为*探索性编程*的系统。探索性编程基于这样的观察:我们大多数程序员大部分时间都在探索和实验。我们尝试使用从未接触过的新 API,以精确了解其行为。我们探索正在开发的算法的行为,看看它如何处理各种类型的数据。我们通过探索不同的输入组合来尝试调试代码。等等……

  1. 目录 {:toc}

nbdev:探索性编程

我们相信探索过程本身非常有价值,而且这个过程应该被保存下来,以便其他程序员(包括六个月后的你自己)能够看到发生了什么,并通过示例学习。可以将其视为类似科学家的日志。您可以使用它来展示您尝试过的内容、哪些成功了、哪些失败了,以及您为深入理解正在工作的系统所做的一切。在这个探索过程中,您会意识到其中一些理解对于系统的正常运行至关重要。因此,探索过程应包含添加测试和断言来确保这种行为。

当您在命令行提示符(或 REPL)上开发,或使用像 Jupyter Notebooks 这样的面向 Notebook 的开发系统时,这种“探索”最容易。但这些系统在“编程”部分不够强大。这就是为什么人们主要在项目早期使用这些系统进行探索,然后切换到 IDE 或文本编辑器。他们切换是为了获得诸如良好的文档查找、良好的语法高亮、与单元测试的集成以及(至关重要的是!)生成最终可分发源代码文件的能力,而不是 Notebook 或 REPL 历史记录。

nbdev 的目的是将 IDE/编辑器开发的关键优势引入 Notebook 系统,这样您就可以在整个生命周期中毫无妥协地在 Notebook 中工作。为了支持这种探索,nbdev 构建在 Jupyter Notebook 之上(这也意味着我们对 Python 的动态特性支持得比普通编辑器或 IDE 要好得多),并添加了以下对软件开发至关重要的工具:

  • 自动为您创建 Python 模块,遵循最佳实践,例如自动使用导出的函数、类和变量定义 __all__更多详情
  • 在标准文本编辑器或 IDE 中导航和编辑代码,并将任何更改自动导出回您的 Notebook
  • 自动从您的代码创建可搜索、超链接的文档;您用反引号包围的任何单词都将超链接到相应的文档,您的文档站点中将为您创建一个侧边栏,其中包含指向每个模块的链接,等等
  • Pip 安装器(为您上传到 PyPI)
  • 测试(直接在您的 Notebook 中定义,并行运行)
  • 持续集成
  • 版本控制冲突处理

这是我们自己用 nbdev 编写的 nbdev 实际“源代码”的片段(当然!)

在 nbdev 源代码中探索 notebook 文件格式

如您所见,当您以这种方式构建软件时,项目团队中的每个人都可以从您在理解问题领域(例如文件格式、性能特征、API 边缘情况等)方面所做的工作中受益。由于开发发生在 notebook 中,您还可以添加图表、文本、链接、图像、视频等,这些内容将自动包含在库的文档中。定义代码的单元格将被隐藏,并替换为您函数标准化文档,显示其名称、参数、文档字符串以及 GitHub 上的源代码链接。

有关 nbdev 的功能、安装和使用方法,请参阅其文档(当然,这些文档是从其源代码自动生成的)。我将在未来几天发布一个逐步教程。在本文的其余部分,我将更多地介绍其背后的历史和背景——*为什么*我们构建了它,以及*为什么*我们以这种方式设计它。首先,让我们回顾一下历史……(如果您对历史不感兴趣,可以直接跳到Jupyter Notebook 缺少了什么。)

软件开发工具

大多数软件开发工具并非基于对探索性编程的思考而构建。大约 30 年前,当我开始编码时,几乎只使用瀑布软件开发。当时在我看来,这种方法—— upfront(事先)事无巨细地定义整个软件系统,然后尽可能贴近规范进行编码——与我实际完成工作的方式完全不符。

然而,在 20 世纪 90 年代,情况开始发生变化。敏捷开发开始流行。人们开始理解大多数软件开发是一个迭代过程的现实,并开发了尊重这一事实的工作方式。然而,我们使用的软件开发工具并没有随着我们工作方式的重大变化而发生重大改变。我们的一些工具库中添加了一些工具,特别是围绕更容易进行测试驱动开发的能力。但这些工具往往只是作为现有编辑器和开发环境的微小扩展出现,而不是真正重新思考开发环境应该是什么样子。

近些年,我们也开始看到人们对探索性测试作为敏捷工具箱的重要组成部分越来越感兴趣。我们完全同意!但我们也认为这远远不够;我们认为在软件开发过程的几乎*每个*部分,探索都应该是故事的核心部分。

传奇人物Donald Knuth远远走在了时代的前面。他希望看到事物以非常不同的方式完成。1983 年,他开发了一种称为文学化编程的方法。他将其描述为:“*一种将编程语言与文档语言相结合的方法,从而使程序比仅用高级语言编写的程序更健壮、更便携、更容易维护,而且可以说写起来更有趣。主要思想是将程序视为一篇文学作品,是写给人类而不是写给计算机的。*”很长一段时间里,我都被这个想法深深吸引,但不幸的是它从未真正普及。使用这种方法所需的工具导致软件开发花费的时间更长,很少有人认为这种折衷是值得的。

将近 30 年后,另一位杰出而具有革命思想的思想家 Bret Victor 表达了他对当前一代开发工具的深切不满,并描述了如何设计“一个用于理解程序的编程系统”。正如他在其开创性演讲“基于原则的创造”中所说:“*我们目前对计算机程序是什么的概念——一个您交给编译器的文本定义列表——直接源自 50 年代末的 Fortran 和 ALGOL。那些语言是为打孔卡设计的。*”

他阐述并用完整的示例说明了一系列用于设计编程系统的新原则。尽管还没有人完全实现他的所有想法,但已经有一些重要的尝试来实现其中的一部分。也许最著名和最完整的实现,包括中间结果的内联显示,是 Chris Lattner 的 Swift 和 Xcode Playgrounds

Xcode 中 Playgrounds 的演示

虽然这是一个很大的进步,但它仍然受到一个并非最初为这种探索而构建的开发环境的基本限制。例如,它完全无法捕捉探索过程,测试无法直接集成到其中,并且无法实现文学化编程的完整丰富愿景。

交互式编程环境

软件开发领域还有一个非常不同的方向,那就是交互式编程(以及相关的实时编程)。它始于几十年前的 LISP 和 Forth REPL,这些允许开发者在运行的应用程序中交互式地添加和删除代码。Smalltalk 更进一步,提供了一个完全交互的可视化工作空间。在所有这些情况下,语言本身都非常适合这种交互式工作,例如 LISP 的宏系统和“代码即数据”的基础。

Smalltalk 中的实时编程 (1980)

尽管这种方法并非当今大多数常规软件开发的模式,但它在科学、统计和其他数据驱动编程的许多领域是最流行的方法。(然而,JavaScript 前端编程越来越多地借鉴了这些方法,例如热重载和浏览器内实时编辑。)例如,Matlab 在 20 世纪 70 年代初期完全是一个交互式工具,如今仍在工程、生物学和其他各种领域广泛使用(它现在也提供常规软件开发功能)。SPLUS 和其开源“表亲”R 也使用了类似的方法,R 在统计和数据可视化社区(以及其他社区)中如今非常流行。

大约 25 年前,当我第一次使用Mathematica 时,我感到特别兴奋。在我看来,Mathematica 是我见过的最接近能支持文学化编程且不牺牲生产力的东西。为此,它使用了“notebook”界面,这个界面很像传统的 REPL,但也允许包含其他类型的信息,包括图表、图像、格式化文本、章节大纲等等。事实上,它不仅没有牺牲生产力,我还发现它实际上让我能够构建以前对我来说遥不可及的东西,因为我可以尝试算法并立即以非常直观的方式获得反馈。

然而,最终 Mathematica 并没有真正帮助我构建任何有用的东西,因为我无法将我的代码或应用程序分发给同事(除非他们花费数千美元购买 Mathematica 许可证来使用它),而且我无法轻松创建供人们从浏览器访问的 Web 应用程序。此外,我发现我的 Mathematica 代码常常比我用其他语言编写的代码慢得多,而且更占用内存。

因此,您可以想象当 Jupyter Notebook 出现时我的兴奋之情。它使用了与 Mathematica 相同的基本 notebook 界面(尽管起初功能较少),但它是开源的,并且允许我使用得到广泛支持且免费的语言进行编写。我不仅使用 Jupyter 来探索算法、API 和新的研究想法,还在 fast.ai 将其用作教学工具。许多学生发现,通过实验输入、查看中间结果和输出,以及尝试自己的修改,有助于他们更全面、更深入地理解讨论的主题。

我们还在完全使用 Jupyter Notebooks 撰写一本书,这是一个非常愉快的经历,它使我们能够将散文、代码示例、分层结构的标题等结合起来,同时确保我们的示例输出(包括图表、表格和图像)始终与代码示例正确匹配。

简而言之:我们非常喜欢使用 Jupyter Notebook,我们发现使用它能完成出色的工作,我们的学生也喜爱它。但我们却没有真正用它来构建我们的软件,这真是太遗憾了!

那么 Jupyter Notebook 缺少了什么?

虽然 Jupyter Notebook 在“探索性编程”的“探索性”部分表现出色,但在“编程”部分则不然。例如,它并没有真正提供执行以下操作的方法:

  • 创建模块化可重用代码,可以在 Jupyter 之外运行
  • 创建带有超链接的可搜索文档
  • 测试代码(包括通过持续集成自动测试)
  • 导航代码
  • 处理版本控制

因此,人们通常不得不在一组集成度不高的工具之间来回切换,从一个工具切换到另一个工具时会产生很大的摩擦,以便获得每种工具的优势

开发方式 优点 缺点
IDE/编辑器
  • 生成最终可分发模块
  • 集成文档查找
  • 集成语法高亮和类型检查
  • 非交互式,难以进行探索
  • 对动态语言支持不完整
  • 文档仅为文本
  • 无法记录交互会话,或通过示例进行解释
REPL/Shell
  • 适合小型交互式探索
  • 除了小型探索外,其他方面都不太好,包括生成可分发模块
传统 Notebook
  • 混合代码、富文本和图片
  • 通过记录交互会话来通过示例进行解释
  • 动态语言的准确代码导航和自动补全
  • 与 REPL 编程有相同缺点

我们决定,处理这些问题的最好方法是尽可能利用现有的出色工具,并在需要时构建自己的工具。例如,处理 Pull Request 和查看 diff,已经有一个很棒的工具:ReviewNB。当您在 ReviewNB 中查看图形化 diff 时,您会突然意识到在纯文本 diff 中一直缺少了多少东西。例如,如果一个提交使您的图像生成模糊了怎么办?或者使您的图表没有标签了怎么办?当您有那个可视化 diff 时,您就真正知道发生了什么。

ReviewNB 中的可视化 diff,显示表格输出的变化

使用 nbdev 可以避免许多合并冲突,因为它为您安装了 Git Hook,这些 Hook 会剥离掉许多最初导致这些冲突的元数据。如果您从 Git pull 时遇到合并冲突,只需运行 nbdev_fix_merge。使用此命令,如果输出中存在冲突,nbdev 将直接使用*您的*单元格输出;如果输入中存在冲突,则最终的 notebook 中会包含*两个*单元格,并带有冲突标记,以便您可以轻松找到它们并直接在 Jupyter 中修复。

基于单元格的 nbdev 合并冲突示例

nbdev 通过简单地创建标准的 Python 模块来创建模块化可重用代码。nbdev 在代码单元格中查找特殊注释,例如 #export,这表明该单元格应导出到一个 Python 模块。通过在 notebook 开头使用特殊注释,每个 notebook 都与一个特定的 Python 模块相关联。一个文档网站(使用Jekyll,因此由GitHub Pages直接支持)会根据 notebook 和特殊注释自动构建。我们编写了自己的文档系统,因为现有的方法(如 Sphinx)无法提供我们所需的所有功能。

对于代码导航,大多数编辑器和 IDE(例如 vim、Emacs 和 vscode)中都已经内置了出色的功能。此外,还有一个额外的惊喜,GitHub 现在甚至在其 Web 界面中直接支持代码导航(测试版,仅在选定项目如 fastai 中提供)!因此,我们确保 nbdev 导出的代码可以在任何这些系统中直接导航和编辑——并且任何编辑都可以自动同步回 notebook。

对于测试,我们编写了自己的简单库和命令行工具。测试直接在 notebook 中编写,作为探索和开发(以及文档)过程的一部分,命令行工具并行运行所有 notebook 中的测试。事实证明,notebook 自然的状态特性是开发单元测试和集成测试的一种绝佳方式。您无需学习创建测试套件的特殊语法,只需使用 Python 中常规的集合和循环结构即可。因此,要学习的新概念少得多。这些测试也可以在您正常的持续集成工具中运行,并且它们能清楚地提供任何测试错误的来源信息。默认的 nbdev 模板包含与 GitHub Actions 的集成,用于持续集成和其他功能(欢迎为其他平台贡献 Pull Request)。

动态 Python

在常规编辑器或 IDE 中完全支持 Python 的一个挑战是,Python 具有特别强大的*动态*特性。例如,您可以随时向类添加方法,可以通过使用Metaclass 系统改变类的创建方式和工作方式,并且可以通过使用装饰器改变函数和方法的行为。微软开发了Language Server Protocol,开发环境可以使用它来获取当前文件和项目的信息,这些信息对于自动补全、代码导航等是必需的。然而,对于像 Python 这样真正的动态语言,这些信息永远只是猜测,因为实际提供正确的信息需要运行 Python 代码本身(出于各种原因,它实际上无法做到这一点——例如,您在编写代码时,代码可能处于某种状态,会删除您的所有文件!)

另一方面,一个 notebook 包含一个实际运行的 Python 解释器实例,您对其拥有完全控制权。因此,Jupyter 可以根据您代码的实际状态提供自动补全、参数列表和上下文相关的文档。例如,在使用Pandas时,我们可以获得 DataFrame 所有列名的 Tab 补全。我们发现 Jupyter Notebook 的这一特性显著提高了探索性编程的效率。我们无需做任何更改就可以让它在 nbdev 中工作得很好;这只是 Jupyter 的一个出色特性,我们通过构建在该平台上就免费获得了。

接下来是什么?

在开发 nbdev 的同时,我们一直在完全使用 nbdev 从头开始编写 fastai v2。fastai v2 提供了一个丰富、结构良好的 API,用于构建深度学习模型。它将在 2020 年上半年发布。它已经功能完备,早期使用者正在用预发布版本构建酷炫的项目。我们还在 fastai v2 中编写了其他项目,其中一些将在未来几周内发布。

我们发现使用 nbdev 的效率是使用传统编程工具的 2 到 3 倍。对我来说,这是一个巨大的惊喜,因为我编码近 30 年,期间尝试了几十种用于构建程序的工具、库和系统。我没想到在生产力方面还有这么大的提升空间。这让我对未来感到兴奋,因为我怀疑在开发人员生产力方面可能还有很大的发展空间,而且我期待看到人们使用 nbdev 构建什么。

如果您决定尝试一下,请务必告诉我们您的体验如何!当然,也请随时提出任何问题。这些讨论的最佳场所是我们为 nbdev 创建的这个论坛帖子。当然,也非常欢迎在nbdev GitHub 仓库中提交 Pull Request。

感谢您对我们项目的关注!


致谢:感谢 Alexis Gallagher 和 Viacheslav Kovalevskyi 对本文草稿提出的有益反馈。感谢 Andrew Shaw 帮助构建 show_doc 的原型,以及 Stas Bekman 在 Git Hook 功能方面的大量贡献。感谢 Hamel Husain 关于使用 GitHub Actions 的有益想法。