青出於藍而勝於藍,一款脫胎於Jupyter Notebook的新型編程環境


不久前,fast.ai 創始研究員 Jeremy Howard 撰文介紹了 fast.ai 最近提出的新型編程環境 nbdev,它基於 Jupyter Notebook 構建,並將 IDE 編輯器的優點帶入 Jupyter Notebook,可以在 Notebooks 中開發而不影響整個項目生命週期。


「我認為,nbdev 是編程環境的一項巨大進步。」——Swift、LLVM 以及 Swift Playgrounds 創造者 Chris Lattner


近年來,我和同事 Sylvain Gugger 一直為熱愛的事情而努力工作,它就是 Python 編程環境 nbdev。nbdev 允許用戶在 Jupyter Notebook 中創建包含測試和豐富文檔系統的完整 Python 包。我們已使用 nbdev 編寫了一個大型編程庫(fastai v2)以及多個小型項目。


青出於藍而勝於藍,一款脫胎於Jupyter Notebook的新型編程環境


nbdev 系統適用於「探索式編程」(exploratory programming)。我們發現,大多數程序員將大部分工作時間用在探索和試驗上。比如我們會試驗從未用過的新型 API,來理解其運作原理;我們探索正在開發的算法的行為,以查看其處理不同數據類型的方式;我們探索不同的輸入組合,來調試代碼……


nbdev:探索式編程


我們認為探索流程是有價值的,應該保存下來,以便其他程序員(或自己)在六個月時間之內能夠看到發生了什麼並通過示例學習。把它看作科學期刊,你可以利用它展示自己嘗試了什麼東西(包括奏效的和無效的),和為了增強對工作系統的理解付出的努力。在探索過程中,你會發現你理解到的某些部分對於系統運行非常關鍵,所以探索應包含測試和斷言(tests and assertions)。


當你基於 prompt(或 REPL)開發,或者使用 notebook-oriented 開發系統(如 Jupyter Notebook)開發時,「探索」是最簡單的。但這些系統的「編程」部分沒有那麼強大。這也是人們主要使用這類系統執行早期探索,然後轉向 IDE 或文本編輯器的原因。


轉而使用其他系統是為了獲得 notebook 或 REPL 不具備的功能,比如:優秀的文檔查找功能、優秀的語法高亮功能、集成單元測試,以及(關鍵的)生成最終可分發源代碼文件的能力。


nbdev 將 IDE/編輯器開發的優勢帶入 notebook 系統中,以便用戶在 notebook 中完成開發,且不會影響整個項目生命週期。為支持此類探索,nbdev 基於 Jupyter Notebook 構建(這意味著,相比普通編輯器或 IDE,nbdev 能夠更好地支持 Python 的動態特性),並針對軟件開發添加了以下重要工具:


  • 遵循最佳實踐自動創建 Python 模塊,如利用導出函數、類和變量自動定義 __all__;
  • 在標準文本編輯器或 IDE 中執行代碼導航和編輯,並將所有更改自動導出回 notebook 中;
  • 基於代碼自動創建可搜索的超鏈接文檔,引號中的任意單詞均被超鏈接至合適的文檔,文檔站點的側邊欄可鏈接至每個模塊等等;
  • pip 安裝包(上傳到 PyPI);
  • 測試(在 notebook 中直接定義,可並行運行);
  • 持續集成;
  • 版本控制和衝突處理。


下圖是 nbdev 真實源代碼中的一個片段,該片段即在 nbdev 中寫成。


青出於藍而勝於藍,一款脫胎於Jupyter Notebook的新型編程環境

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


如上圖所示,用這種方式構建軟件時,項目團隊中的所有成員均可以從你為理解問題域所做的工作中獲益,如文件格式、性能特點、API 邊緣案例(edge case)等。由於開發過程在 notebook 中進行,因此你還可以添加圖表、文本、鏈接、圖像、視頻等,這些將被自動納入庫文檔中。定義代碼的單元格將被隱藏,並被標準化函數文檔代替,從而展示其名稱、參數、文檔字符串和源代碼 GitHub 鏈接。


關於 nbdev 特性、安裝和使用的更多信息,參見 nbdev 文檔:https://nbdev.fast.ai/。


下文將介紹構建 nbdev 的原因以及 nbdev 設計原理背後的歷史和背景。首先,我們先來了解歷史。(如果你對此不感興趣,可以跳至「Jupyter Notebook 少了什麼?」)


軟件開發工具


大部分軟件開發工具不是基於探索式編程創建的。大約 30 年前我剛開始寫代碼時,瀑布軟件開發幾乎處於壟斷地位。這種編程方法預先詳細定義整個軟件系統,然後在編程時儘可能地靠近規格。那時我便認為,這種方法並不適合我的工作方式。


1990 年代,事情出現變化,敏捷開發開始流行。人們開始理解「大部分軟件開發是迭代過程」這一現實,並開發出符合這一事實的工作方式。但是,當時我們使用的軟件開發工具並沒能完成變革,去匹配工作方式的改變。一些工具被添加到庫中,用來更輕鬆地執行測試驅動開發。但這些工具只是現有編輯器和開發環境的輕度擴展,並沒有真正去重新思考開發環境應該是什麼樣子


探索式測試是敏捷測試的重要組成部分,

近年來,人們對探索式測試的興趣逐漸增長。我們絕對贊同這一點,但我們認為走的還不夠遠。我們認為在軟件開發流程的每個部分中,探索都應當成為核心


傳奇人物 Donald Knuth 走在時代前列,他想看到不同的開發方式。1983 年,他提出了一種叫做「文學式編程」的方法,並將其描述為「結合編程語言和文檔語言,從而使寫出來的程序比僅用高級語言編寫的程序更加穩健、更具可移植性、更容易維護、編寫時更富有樂趣。其主要思想是將程序看作受眾為人類而非計算機的文學作品。」


在很長一段時間裡我為這個想法而痴迷,但很不幸這個想法並沒有成功。因為這樣會致軟件開發時間變長,沒人認願意付出這種代價。


將近 30 年後,另一位變革性的思想家 Bret Victor 表達了對當時開發工具的深刻不滿,並描述瞭如何設計「理解程序的編程系統」。他在突破性演講「Inventing on Principle」中表示:「我們現在的計算機程序概念是一串文本定義,你把它們傳遞到基於 1950 年代末 Fortran 和 ALGOL 直接得到的編譯器。但是 Fortran 和 ALGOL 語言是為穿孔卡片設計的啊。」


他提出了完善的示例,以及多項編程系統設計新原則。儘管沒人完全實現他的全部想法,但已經有人嘗試實現其中的一部分。或許最知名也最完整的實現(包含對中間結果的展示)是 Chris Lattner 創建的 Swift 和 Xcode Playgrounds。


青出於藍而勝於藍,一款脫胎於Jupyter Notebook的新型編程環境

Xcode Playgrounds 的演示圖。


儘管這是一次重要飛躍,但它仍然受限於一項基本限制,即開發環境的構建初衷並不涉及此類探索。例如,開發環境無法捕捉探索過程,測試不能直接集成到開發環境內,無法實現文學式編程的完善版本。


交互式編程環境


軟件開發還有一個不同的方向,即交互式編程(以及相關的實時編程)。對交互式編程的嘗試在幾十年前已經出現,如 LISP 和 Forth REPL,它們允許開發者在運行的應用程序中交互式地添加和移除代碼。Smalltalk 將其又推進了一步,它提供了完全交互式的視覺工作區。在所有這些案例中,語言本身與交互式工作方式適配良好,如 LISP 的宏系統和「code as data」基礎。


青出於藍而勝於藍,一款脫胎於Jupyter Notebook的新型編程環境

Smalltalk 語言中的實時編程(1980)。


在今天,該方法不是最常規的軟件開發方式,但它是科學、統計學和其他數據驅動編程等多個領域中最流行的方法。(JavaScript 前端編程不斷從這些方法中借鑑思路,如 hot reloading 和瀏覽器內實時編輯。)例如,1970 年代 Matlab 剛出現時是完全交互式的工具,現在仍廣泛用於工程、生物學等領域(目前它還提供常規軟件開發功能)。S-PLUS 也使用過類似的方法,與 S-PLUS 有關聯的開源語言 R 目前在統計和數據可視化社區中非常流行。


25 年前我第一次使用 Mathematica 時非常興奮。對我而言,Mathematica 是最有可能支持文學式編程的語言,且不會影響生產效率。Mathematica 使用「notebook」界面,其行為類似傳統的 REPL,但允許其他類型的信息,如圖表、圖像、格式化文本、大綱部分等。事實上,它不僅沒有影響生產效率,我還使用它構建出了之前無法構建的東西。它幫助我在試驗算法後立即得到視覺化反饋。


最終,Mathematica 並沒有幫助我構建出任何有用的東西,因為我無法把自己的代碼或應用分發給同事(除非他們花數千美元購買 Mathematica 許可證),無法輕鬆創建瀏覽器內可用的 web 應用。此外,我發現 Mathematica 代碼通常比使用其他語言寫的代碼更慢、更耗費內存。


因此,你可以想象 Jupyter Notebook 誕生時我有多興奮。Jupyter Notebook 和 Mathematica 的基礎 notebook 界面一樣(儘管最初 Jupyter Notebook 的界面只有後者的一小部分功能),而且開源了,這樣我就可以使用廣泛支持和免費可用的語言寫代碼。我曾使用 Jupyter 探索算法、API 和新的研究想法,還把它作為 fast.ai 的教學工具。很多學生髮現它具備試驗輸入、查看中間結果和輸出的能力,且允許修改,從而幫助他們更完備、深刻地理解正在討論的主題。


我們還使用 Jupyter Notebook 寫了一本書,這是一件很有趣的事。基於 Jupyter Notebook,我們在書中結合了 prose、代碼示例、層級結構化標題等,同時保證樣本輸出(包含圖表、表格和圖像)完美匹配代碼示例。


簡而言之:我們真的喜歡用 Jupyter Notebook,並利用它做出了很棒的作品,學生也喜歡它。但是我們竟然沒法用它來構建自己的軟件!


Jupyter Notebook 少了什麼?


Jupyter Notebook 擅長「探索式編程」中的「探索」部分,但它不太擅長「編程」。例如,它沒有提供執行以下操作的方式:


  • 創建模塊化可重用代碼,這些代碼可在 Jupyter 外部運行;
  • 創建可搜索超鏈接文檔;
  • 測試代碼(包括通過持續集成實現的自動化代碼測試);
  • 代碼導航;
  • 版本控制。


因此,開發者通常需要在未得到良好集成的工具間轉換,以獲取這些工具的優勢,而在工具間來回轉換會導致衝突。不同工具的優勢如下所示:


青出於藍而勝於藍,一款脫胎於Jupyter Notebook的新型編程環境


我們認為處理這些衝突的最好方法是,利用現有的好用工具構建所需的功能。例如,對於處理 pull request 和查看 diff,已經存在一個好用工具:ReviewNB。當你在 ReviewNB 中查看圖解版 diff 時,你會突然發現純文本 diff 中的遺漏信息。例如,如果某個 commit 使圖像生成結果變得模糊不清,或者使圖表沒有標籤該怎麼辦?當你將這些 diff 視覺化呈現時,你會確切瞭解到底發生了什麼。


青出於藍而勝於藍,一款脫胎於Jupyter Notebook的新型編程環境

ReviewNB 中的視覺化 diff,展示了表格輸出的更改。


nbdev 避免了很多合併衝突,因為它安裝了 git hook,從而首先去除引發衝突的部分元數據。如果你執行 git pull 時出現合併衝突,只需運行 nbdev_fix_merge 即可。運行該命令時,nbdev 只需使用輸出存在衝突的單元格輸出,如果單元格輸入存在衝突,那麼最終 notebook 中會包含兩個單元格以及衝突標記。這樣你就可以輕鬆找出它們,並在 Jupyter 中直接修復。


青出於藍而勝於藍,一款脫胎於Jupyter Notebook的新型編程環境

nbdev 中基於單元格的合併衝突示例。


nbdev 只需創建標準 Python 模塊,即可創建模塊化可重用代碼。nbdev 尋找代碼單元格中的特殊註釋,如 #export(表示該單元格應被導出至 Python 模塊)。在 notebook 開頭處使用特殊註釋,可將每個 notebook 與特定 Python 模塊結合起來。文檔站點(使用 Jekyll,以便得到 GitHub Pages 的直接支持)基於 notebook 和特殊註釋自動創建。我們編寫了自己的文檔系統,因為現有方法(如 Sphinx)無法提供我們所需的全部功能。


至於代碼導航,大部分編輯器和 IDE(如 vim、Emacs 和 vscode)中內置有一些不錯的功能。GitHub 的網頁界面甚至直接支持代碼導航(目前尚處於測試階段,僅針對特定選中項目,如 fast.ai)。因此我們確保 nbdev 導出的代碼可在任意系統中直接導航和編輯,且任意編輯均被自動同步至 notebook。


至於測試,我們已經編寫了自己的簡單庫和命令行工具。作為探索和開發(以及文檔)流程的一部分,測試可直接在 notebook 中編寫,命令行工具在所有 notebook 中並行運行測試。notebook 的天然有狀態(natural statefulness)是開發單元測試和集成測試的重要方式。你無需使用特殊語法來學習創建測試套件,只需使用 Python 中的常規 collection 和 looping 結構,這樣要學習的新概念就少得多了。


這些測試還可以在普通的持續集成工具中運行,它們對測試錯誤源提供明確信息。默認 nbdev 模板集成了 GitHub Actions,以實現持續集成等功能。


動態 Python


在常規編輯器或 IDE 中完全支持 Python 的一大挑戰是,Python 具備強大的動態特性。例如,你可以在任意時間向類中添加方法,使用元類系統改變創建類的方式以及類的工作方式,使用裝飾器改變函數和方法的運行方式。微軟開發了 Language Server Protocol,可用於開發環境,以獲取自動補全、代碼導航等所需的當前文件和項目信息。但是,對於真正動態的語言(如 Python),此類信息通常只是猜測,因為提供正確信息需要運行 Python 代碼(出於種種原因,Python 無法執行該操作,例如寫代碼時代碼可能處於混亂狀態,導致所有文件被刪除)。


另一方面,notebook 包含實際運行的 Python 解釋器實例,這完全在你的掌控之中。因此,Jupyter 可以基於代碼的實際狀態提供自動補全、參數列表和上下文相關文檔。例如,在使用 Pandas 時,我們得到 DataFrames 所有列名的 tab 自動補全。我們發現 Jupyter Notebook 的這一特性提高了探索式編程的生產效率。無需作出任何更改,它就能在 nbdev 中良好運行。而這只是基於 Jupyter Notebook 構建開發環境所免費獲取的部分 Jupyter 功能而已。


現狀


伴隨著 nbdev 的開發,我們使用 nbdev 從頭編寫了 fastai v2。fastai v2 為構建深度學習模型提供豐富、結構完善的 API,將於 2020 年上半年發佈。目前其功能完善,早期使用者已經使用預發佈版本搭建了很酷的項目。我們還在 fastai v2 中編寫了其他項目,其中一些將在未來幾周發佈。


我們發現使用 nbdev 比使用傳統編程工具的生產效率高 1-2 倍。對我而言這是一個巨大的驚喜。我已經寫了 30 多年代碼,試過幾十個構建程序的工具、庫和系統,我原本沒想到生產效率還有如此大的提升空間。現在,我對未來感到振奮,我覺得開發者效率還有很大的提升空間,我期望看到人們用 nbdev 創建新的項目。


分享到:


相關文章: