青出于藍(lán)而勝于藍(lán),這是一款脫胎于Jupyter Notebook的新型編程環(huán)境
不久前,fast.ai 創(chuàng)始研究員 Jeremy Howard 撰文介紹了 fast.ai 最近提出的新型編程環(huán)境 nbdev,它基于 Jupyter Notebook 構(gòu)建,并將 IDE 編輯器的優(yōu)點(diǎn)帶入 Jupyter Notebook,可以在 Notebooks 中開發(fā)而不影響整個(gè)項(xiàng)目生命周期。
- nbdev GitHub 地址:https://github.com/fastai/nbdev/
- nbdev 文檔:https://nbdev.fast.ai/
「我認(rèn)為,nbdev 是編程環(huán)境的一項(xiàng)巨大進(jìn)步?!?mdash;—Swift、LLVM 以及 Swift Playgrounds 創(chuàng)造者 Chris Lattner
近年來,我和同事 Sylvain Gugger 一直為熱愛的事情而努力工作,它就是 Python 編程環(huán)境 nbdev。nbdev 允許用戶在 Jupyter Notebook 中創(chuàng)建包含測(cè)試和豐富文檔系統(tǒng)的完整 Python 包。我們已使用 nbdev 編寫了一個(gè)大型編程庫(fastai v2)以及多個(gè)小型項(xiàng)目。
本文作者、fast.ai創(chuàng)始研究員Jeremy Howard。
nbdev 系統(tǒng)適用于「探索式編程」(exploratory programming)。我們發(fā)現(xiàn),大多數(shù)程序員將大部分工作時(shí)間用在探索和試驗(yàn)上。比如我們會(huì)試驗(yàn)從未用過的新型 API,來理解其運(yùn)作原理;我們探索正在開發(fā)的算法的行為,以查看其處理不同數(shù)據(jù)類型的方式;我們探索不同的輸入組合,來調(diào)試代碼……
nbdev:探索式編程
我們認(rèn)為探索流程是有價(jià)值的,應(yīng)該保存下來,以便其他程序員(或自己)在六個(gè)月時(shí)間之內(nèi)能夠看到發(fā)生了什么并通過示例學(xué)習(xí)。把它看作科學(xué)期刊,你可以利用它展示自己嘗試了什么東西(包括奏效的和無效的),和為了增強(qiáng)對(duì)工作系統(tǒng)的理解付出的努力。在探索過程中,你會(huì)發(fā)現(xiàn)你理解到的某些部分對(duì)于系統(tǒng)運(yùn)行非常關(guān)鍵,所以探索應(yīng)包含測(cè)試和斷言(tests and assertions)。
當(dāng)你基于 prompt(或 REPL)開發(fā),或者使用 notebook-oriented 開發(fā)系統(tǒng)(如 Jupyter Notebook)開發(fā)時(shí),「探索」是最簡單的。但這些系統(tǒng)的「編程」部分沒有那么強(qiáng)大。這也是人們主要使用這類系統(tǒng)執(zhí)行早期探索,然后轉(zhuǎn)向 IDE 或文本編輯器的原因。
轉(zhuǎn)而使用其他系統(tǒng)是為了獲得 notebook 或 REPL 不具備的功能,比如:優(yōu)秀的文檔查找功能、優(yōu)秀的語法高亮功能、集成單元測(cè)試,以及(關(guān)鍵的)生成最終可分發(fā)源代碼文件的能力。
nbdev 將 IDE/編輯器開發(fā)的優(yōu)勢(shì)帶入 notebook 系統(tǒng)中,以便用戶在 notebook 中完成開發(fā),且不會(huì)影響整個(gè)項(xiàng)目生命周期。為支持此類探索,nbdev 基于 Jupyter Notebook 構(gòu)建(這意味著,相比普通編輯器或 IDE,nbdev 能夠更好地支持 Python 的動(dòng)態(tài)特性),并針對(duì)軟件開發(fā)添加了以下重要工具:
- 遵循最佳實(shí)踐自動(dòng)創(chuàng)建 Python 模塊,如利用導(dǎo)出函數(shù)、類和變量自動(dòng)定義 __all__;
- 在標(biāo)準(zhǔn)文本編輯器或 IDE 中執(zhí)行代碼導(dǎo)航和編輯,并將所有更改自動(dòng)導(dǎo)出回 notebook 中;
- 基于代碼自動(dòng)創(chuàng)建可搜索的超鏈接文檔,引號(hào)中的任意單詞均被超鏈接至合適的文檔,文檔站點(diǎn)的側(cè)邊欄可鏈接至每個(gè)模塊等等;
- pip 安裝包(上傳到 PyPI);
- 測(cè)試(在 notebook 中直接定義,可并行運(yùn)行);
- 持續(xù)集成;
- 版本控制和沖突處理。
下圖是 nbdev 真實(shí)源代碼中的一個(gè)片段,該片段即在 nbdev 中寫成。
在 nbdev 源代碼中探索 notebook 文件格式。
如上圖所示,用這種方式構(gòu)建軟件時(shí),項(xiàng)目團(tuán)隊(duì)中的所有成員均可以從你為理解問題域所做的工作中獲益,如文件格式、性能特點(diǎn)、API 邊緣案例(edge case)等。由于開發(fā)過程在 notebook 中進(jìn)行,因此你還可以添加圖表、文本、鏈接、圖像、視頻等,這些將被自動(dòng)納入庫文檔中。定義代碼的單元格將被隱藏,并被標(biāo)準(zhǔn)化函數(shù)文檔代替,從而展示其名稱、參數(shù)、文檔字符串和源代碼 GitHub 鏈接。
關(guān)于 nbdev 特性、安裝和使用的更多信息,參見 nbdev 文檔:https://nbdev.fast.ai/。
下文將介紹構(gòu)建 nbdev 的原因以及 nbdev 設(shè)計(jì)原理背后的歷史和背景。首先,我們先來了解歷史。(如果你對(duì)此不感興趣,可以跳至「Jupyter Notebook 少了什么?」)
軟件開發(fā)工具
大部分軟件開發(fā)工具不是基于探索式編程創(chuàng)建的。大約 30 年前我剛開始寫代碼時(shí),瀑布軟件開發(fā)幾乎處于壟斷地位。這種編程方法預(yù)先詳細(xì)定義整個(gè)軟件系統(tǒng),然后在編程時(shí)盡可能地靠近規(guī)格。那時(shí)我便認(rèn)為,這種方法并不適合我的工作方式。
1990 年代,事情出現(xiàn)變化,敏捷開發(fā)開始流行。人們開始理解「大部分軟件開發(fā)是迭代過程」這一現(xiàn)實(shí),并開發(fā)出符合這一事實(shí)的工作方式。但是,當(dāng)時(shí)我們使用的軟件開發(fā)工具并沒能完成變革,去匹配工作方式的改變。一些工具被添加到庫中,用來更輕松地執(zhí)行測(cè)試驅(qū)動(dòng)開發(fā)。但這些工具只是現(xiàn)有編輯器和開發(fā)環(huán)境的輕度擴(kuò)展,并沒有真正去重新思考開發(fā)環(huán)境應(yīng)該是什么樣子。
探索式測(cè)試是敏捷測(cè)試的重要組成部分,近年來,人們對(duì)探索式測(cè)試的興趣逐漸增長。我們絕對(duì)贊同這一點(diǎn),但我們認(rèn)為走的還不夠遠(yuǎn)。我們認(rèn)為在軟件開發(fā)流程的每個(gè)部分中,探索都應(yīng)當(dāng)成為核心。
傳奇人物 Donald Knuth 走在時(shí)代前列,他想看到不同的開發(fā)方式。1983 年,他提出了一種叫做「文學(xué)式編程」的方法,并將其描述為「結(jié)合編程語言和文檔語言,從而使寫出來的程序比僅用高級(jí)語言編寫的程序更加穩(wěn)健、更具可移植性、更容易維護(hù)、編寫時(shí)更富有樂趣。其主要思想是將程序看作受眾為人類而非計(jì)算機(jī)的文學(xué)作品?!?/p>
在很長一段時(shí)間里我為這個(gè)想法而癡迷,但很不幸這個(gè)想法并沒有成功。因?yàn)檫@樣會(huì)致軟件開發(fā)時(shí)間變長,沒人認(rèn)愿意付出這種代價(jià)。
將近 30 年后,另一位變革性的思想家 Bret Victor 表達(dá)了對(duì)當(dāng)時(shí)開發(fā)工具的深刻不滿,并描述了如何設(shè)計(jì)「理解程序的編程系統(tǒng)」。他在突破性演講「Inventing on Principle」中表示:「我們現(xiàn)在的計(jì)算機(jī)程序概念是一串文本定義,你把它們傳遞到基于 1950 年代末 Fortran 和 ALGOL 直接得到的編譯器。但是 Fortran 和 ALGOL 語言是為穿孔卡片設(shè)計(jì)的啊?!?/p>
他提出了完善的示例,以及多項(xiàng)編程系統(tǒng)設(shè)計(jì)新原則。盡管沒人完全實(shí)現(xiàn)他的全部想法,但已經(jīng)有人嘗試實(shí)現(xiàn)其中的一部分。或許最知名也最完整的實(shí)現(xiàn)(包含對(duì)中間結(jié)果的展示)是 Chris Lattner 創(chuàng)建的 Swift 和 Xcode Playgrounds。
Xcode Playgrounds 的演示圖。
盡管這是一次重要飛躍,但它仍然受限于一項(xiàng)基本限制,即開發(fā)環(huán)境的構(gòu)建初衷并不涉及此類探索。例如,開發(fā)環(huán)境無法捕捉探索過程,測(cè)試不能直接集成到開發(fā)環(huán)境內(nèi),無法實(shí)現(xiàn)文學(xué)式編程的完善版本。
交互式編程環(huán)境
軟件開發(fā)還有一個(gè)不同的方向,即交互式編程(以及相關(guān)的實(shí)時(shí)編程)。對(duì)交互式編程的嘗試在幾十年前已經(jīng)出現(xiàn),如 LISP 和 Forth REPL,它們?cè)试S開發(fā)者在運(yùn)行的應(yīng)用程序中交互式地添加和移除代碼。Smalltalk 將其又推進(jìn)了一步,它提供了完全交互式的視覺工作區(qū)。在所有這些案例中,語言本身與交互式工作方式適配良好,如 LISP 的宏系統(tǒng)和「code as data」基礎(chǔ)。
Smalltalk 語言中的實(shí)時(shí)編程(1980)。
在今天,該方法不是最常規(guī)的軟件開發(fā)方式,但它是科學(xué)、統(tǒng)計(jì)學(xué)和其他數(shù)據(jù)驅(qū)動(dòng)編程等多個(gè)領(lǐng)域中最流行的方法。(JavaScript 前端編程不斷從這些方法中借鑒思路,如 hot reloading 和瀏覽器內(nèi)實(shí)時(shí)編輯。)例如,1970 年代 Matlab 剛出現(xiàn)時(shí)是完全交互式的工具,現(xiàn)在仍廣泛用于工程、生物學(xué)等領(lǐng)域(目前它還提供常規(guī)軟件開發(fā)功能)。S-PLUS 也使用過類似的方法,與 S-PLUS 有關(guān)聯(lián)的開源語言 R 目前在統(tǒng)計(jì)和數(shù)據(jù)可視化社區(qū)中非常流行。
25 年前我第一次使用 Mathematica 時(shí)非常興奮。對(duì)我而言,Mathematica 是最有可能支持文學(xué)式編程的語言,且不會(huì)影響生產(chǎn)效率。Mathematica 使用「notebook」界面,其行為類似傳統(tǒng)的 REPL,但允許其他類型的信息,如圖表、圖像、格式化文本、大綱部分等。事實(shí)上,它不僅沒有影響生產(chǎn)效率,我還使用它構(gòu)建出了之前無法構(gòu)建的東西。它幫助我在試驗(yàn)算法后立即得到視覺化反饋。
最終,Mathematica 并沒有幫助我構(gòu)建出任何有用的東西,因?yàn)槲覠o法把自己的代碼或應(yīng)用分發(fā)給同事(除非他們花數(shù)千美元購買 Mathematica 許可證),無法輕松創(chuàng)建瀏覽器內(nèi)可用的 web 應(yīng)用。此外,我發(fā)現(xiàn) Mathematica 代碼通常比使用其他語言寫的代碼更慢、更耗費(fèi)內(nèi)存。
因此,你可以想象 Jupyter Notebook 誕生時(shí)我有多興奮。Jupyter Notebook 和 Mathematica 的基礎(chǔ) notebook 界面一樣(盡管最初 Jupyter Notebook 的界面只有后者的一小部分功能),而且開源了,這樣我就可以使用廣泛支持和免費(fèi)可用的語言寫代碼。我曾使用 Jupyter 探索算法、API 和新的研究想法,還把它作為 fast.ai 的教學(xué)工具。很多學(xué)生發(fā)現(xiàn)它具備試驗(yàn)輸入、查看中間結(jié)果和輸出的能力,且允許修改,從而幫助他們更完備、深刻地理解正在討論的主題。
我們還使用 Jupyter Notebook 寫了一本書,這是一件很有趣的事?;?Jupyter Notebook,我們?cè)跁薪Y(jié)合了 prose、代碼示例、層級(jí)結(jié)構(gòu)化標(biāo)題等,同時(shí)保證樣本輸出(包含圖表、表格和圖像)完美匹配代碼示例。
簡而言之:我們真的喜歡用 Jupyter Notebook,并利用它做出了很棒的作品,學(xué)生也喜歡它。但是我們竟然沒法用它來構(gòu)建自己的軟件!
Jupyter Notebook 少了什么?
Jupyter Notebook 擅長「探索式編程」中的「探索」部分,但它不太擅長「編程」。例如,它沒有提供執(zhí)行以下操作的方式:
- 創(chuàng)建模塊化可重用代碼,這些代碼可在 Jupyter 外部運(yùn)行;
- 創(chuàng)建可搜索超鏈接文檔;
- 測(cè)試代碼(包括通過持續(xù)集成實(shí)現(xiàn)的自動(dòng)化代碼測(cè)試);
- 代碼導(dǎo)航;
- 版本控制。
因此,開發(fā)者通常需要在未得到良好集成的工具間轉(zhuǎn)換,以獲取這些工具的優(yōu)勢(shì),而在工具間來回轉(zhuǎn)換會(huì)導(dǎo)致沖突。不同工具的優(yōu)勢(shì)如下所示:
我們認(rèn)為處理這些沖突的最好方法是,利用現(xiàn)有的好用工具構(gòu)建所需的功能。例如,對(duì)于處理 pull request 和查看 diff,已經(jīng)存在一個(gè)好用工具:ReviewNB。當(dāng)你在 ReviewNB 中查看圖解版 diff 時(shí),你會(huì)突然發(fā)現(xiàn)純文本 diff 中的遺漏信息。例如,如果某個(gè) commit 使圖像生成結(jié)果變得模糊不清,或者使圖表沒有標(biāo)簽該怎么辦?當(dāng)你將這些 diff 視覺化呈現(xiàn)時(shí),你會(huì)確切了解到底發(fā)生了什么。
ReviewNB 中的視覺化 diff,展示了表格輸出的更改。
nbdev 避免了很多合并沖突,因?yàn)樗惭b了 git hook,從而首先去除引發(fā)沖突的部分元數(shù)據(jù)。如果你執(zhí)行 git pull 時(shí)出現(xiàn)合并沖突,只需運(yùn)行 nbdev_fix_merge 即可。運(yùn)行該命令時(shí),nbdev 只需使用輸出存在沖突的單元格輸出,如果單元格輸入存在沖突,那么最終 notebook 中會(huì)包含兩個(gè)單元格以及沖突標(biāo)記。這樣你就可以輕松找出它們,并在 Jupyter 中直接修復(fù)。
nbdev 中基于單元格的合并沖突示例。
nbdev 只需創(chuàng)建標(biāo)準(zhǔn) Python 模塊,即可創(chuàng)建模塊化可重用代碼。nbdev 尋找代碼單元格中的特殊注釋,如 #export(表示該單元格應(yīng)被導(dǎo)出至 Python 模塊)。在 notebook 開頭處使用特殊注釋,可將每個(gè) notebook 與特定 Python 模塊結(jié)合起來。文檔站點(diǎn)(使用 Jekyll,以便得到 GitHub Pages 的直接支持)基于 notebook 和特殊注釋自動(dòng)創(chuàng)建。我們編寫了自己的文檔系統(tǒng),因?yàn)楝F(xiàn)有方法(如 Sphinx)無法提供我們所需的全部功能。
至于代碼導(dǎo)航,大部分編輯器和 IDE(如 vim、Emacs 和 vscode)中內(nèi)置有一些不錯(cuò)的功能。GitHub 的網(wǎng)頁界面甚至直接支持代碼導(dǎo)航(目前尚處于測(cè)試階段,僅針對(duì)特定選中項(xiàng)目,如 fast.ai)。因此我們確保 nbdev 導(dǎo)出的代碼可在任意系統(tǒng)中直接導(dǎo)航和編輯,且任意編輯均被自動(dòng)同步至 notebook。
至于測(cè)試,我們已經(jīng)編寫了自己的簡單庫和命令行工具。作為探索和開發(fā)(以及文檔)流程的一部分,測(cè)試可直接在 notebook 中編寫,命令行工具在所有 notebook 中并行運(yùn)行測(cè)試。notebook 的天然有狀態(tài)(natural statefulness)是開發(fā)單元測(cè)試和集成測(cè)試的重要方式。你無需使用特殊語法來學(xué)習(xí)創(chuàng)建測(cè)試套件,只需使用 Python 中的常規(guī) collection 和 looping 結(jié)構(gòu),這樣要學(xué)習(xí)的新概念就少得多了。
這些測(cè)試還可以在普通的持續(xù)集成工具中運(yùn)行,它們對(duì)測(cè)試錯(cuò)誤源提供明確信息。默認(rèn) nbdev 模板集成了 GitHub Actions,以實(shí)現(xiàn)持續(xù)集成等功能。
動(dòng)態(tài) Python
在常規(guī)編輯器或 IDE 中完全支持 Python 的一大挑戰(zhàn)是,Python 具備強(qiáng)大的動(dòng)態(tài)特性。例如,你可以在任意時(shí)間向類中添加方法,使用元類系統(tǒng)改變創(chuàng)建類的方式以及類的工作方式,使用裝飾器改變函數(shù)和方法的運(yùn)行方式。微軟開發(fā)了 Language Server Protocol,可用于開發(fā)環(huán)境,以獲取自動(dòng)補(bǔ)全、代碼導(dǎo)航等所需的當(dāng)前文件和項(xiàng)目信息。但是,對(duì)于真正動(dòng)態(tài)的語言(如 Python),此類信息通常只是猜測(cè),因?yàn)樘峁┱_信息需要運(yùn)行 Python 代碼(出于種種原因,Python 無法執(zhí)行該操作,例如寫代碼時(shí)代碼可能處于混亂狀態(tài),導(dǎo)致所有文件被刪除)。
另一方面,notebook 包含實(shí)際運(yùn)行的 Python 解釋器實(shí)例,這完全在你的掌控之中。因此,Jupyter 可以基于代碼的實(shí)際狀態(tài)提供自動(dòng)補(bǔ)全、參數(shù)列表和上下文相關(guān)文檔。例如,在使用 Pandas 時(shí),我們得到 DataFrames 所有列名的 tab 自動(dòng)補(bǔ)全。我們發(fā)現(xiàn) Jupyter Notebook 的這一特性提高了探索式編程的生產(chǎn)效率。無需作出任何更改,它就能在 nbdev 中良好運(yùn)行。而這只是基于 Jupyter Notebook 構(gòu)建開發(fā)環(huán)境所免費(fèi)獲取的部分 Jupyter 功能而已。
現(xiàn)狀
伴隨著 nbdev 的開發(fā),我們使用 nbdev 從頭編寫了 fastai v2。fastai v2 為構(gòu)建深度學(xué)習(xí)模型提供豐富、結(jié)構(gòu)完善的 API,將于 2020 年上半年發(fā)布。目前其功能完善,早期使用者已經(jīng)使用預(yù)發(fā)布版本搭建了很酷的項(xiàng)目。我們還在 fastai v2 中編寫了其他項(xiàng)目,其中一些將在未來幾周發(fā)布。
我們發(fā)現(xiàn)使用 nbdev 比使用傳統(tǒng)編程工具的生產(chǎn)效率高 1-2 倍。對(duì)我而言這是一個(gè)巨大的驚喜。我已經(jīng)寫了 30 多年代碼,試過幾十個(gè)構(gòu)建程序的工具、庫和系統(tǒng),我原本沒想到生產(chǎn)效率還有如此大的提升空間?,F(xiàn)在,我對(duì)未來感到振奮,我覺得開發(fā)者效率還有很大的提升空間,我期望看到人們用 nbdev 創(chuàng)建新的項(xiàng)目。