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