[譯] 如何在資料科學中寫出生產級別的程式碼?

sisibeloved發表於2018-08-20

編寫生產級別的程式碼的能力是資料科學家夢寐以求的技能之一 —— 無論職位要求上是否明確的要求。對於由軟體工程師轉型的資料科學家來說這可能沒什麼難度,畢竟他們也許已經在生產程式碼的開發和部署上有著豐富的經驗。

這篇文章是針對那些剛開始編寫生產級程式碼並有興趣學習它的人,比如大學的應屆畢業生或從事資料科學(和計劃轉型)的專業人員。對於他們來說,編寫生產級程式碼看上去是一項艱鉅的任務。

我會介紹幾個編寫生產級別程式碼的技巧,請多加練習,此外這篇文章不需要用到任何資料科學方面的專業知識。

1. 保持模組化

這對於任何軟體工程師來說都是需要掌握的基本技巧。它的核心思想是把龐大的程式碼塊基於其功能分割成一個個小型的獨立程式碼段(函式)。它由兩部分組成。

(i) 將程式碼拆成小塊,每一塊執行特定的功能(可以包含子功能)。

(ii) 將這些函式基於用途組合成模組(或 Python 檔案)。這也有助於保持程式碼的有序性和可維護性。

首先將龐大的程式碼塊分解成許多簡單函式,每一個都包含特定格式的輸入和輸出。如上所述,每個函式應實現單一職責,如 清除資料中的離群點、替換謬誤值、對模型進行評分、計算標準差(RMSE,又譯作均方根差) 等等。嘗試將這些函式繼續分解成執行更小單元的子任務的函式,直至無法拆分。

底層函式 —— 無法再進一步分解的基本函式。比如,計算資料的標準差(RMSE)或標準分數(Z-score)。其中的某些函式可以廣泛應用於實現演算法或訓練機器學習模型。

中間層函式 —— 使用一個或多個底層函式和/或其他中間層函式來實現功能。舉個例子,清除資料中的離群點 函式會使用 計算標準分數 函式來清除離群點,只保留特定邊界內的資料;誤差 函式會使用 計算標準差 函式來獲取標準差。

上層函式 —— 使用一個或多箇中間層函式以及底層函式來實現功能。打個比方,模型訓練函式使用了隨機獲取標本資料函式、模型評估函式和矩陣函式等多個函式。

最後,將所有能夠複用的底層和中間層函式分到一個 Python 檔案中(可以作為模組匯入),將所有其它的專用的底層和中間層函式分到另一個 Python 檔案中。所有高階函式應該歸到同一個單獨的 Python 檔案中。這個 Python 檔案為演算法開發中的每一步提供指引 —— 從組合多源資料到機器學習模型的構建。

儘管無須墨守成規,但我還是推薦你按這個流程,一步一個腳印,直至培養出自己的程式碼風格。

2. 日誌和監測工具

日誌和監測工具(LI)就像飛機上的黑匣子,負責記錄駕駛艙中的一切。LI 的主要目的是記錄程式碼執行時的有效資訊,以便開發者在錯誤發生時除錯和提升程式碼效能(比如減少執行時間)。

那麼日誌和監測工具有什麼區別呢?

(i) 日誌記錄 —— 只記錄可操作的資訊,如執行期間的關鍵故障和諸如程式碼本身稍後將用到的中間結果之類的結構化資料。在開發和測試階段可以使用多種日誌級別,如 debug、info、warn 和 error。然而,在生產過程中需要不惜一切代價來避免這麼做。

日誌記錄應儘量簡潔,只包含需要引起維護者注意的和需要立即處理的資訊。

(ii) 監測工具 —— 記錄所有日誌中遺漏的其它資訊,這將幫助我們驗證程式碼執行的步驟,並在必要時為改進效能提供幫助。資料越多,監測工具能給的資訊就越多。

驗證程式碼執行步驟 —— 我們應該記錄諸如任務名稱、中間結果、步驟經過等資訊,這將有助於我們驗證結果,並確認演算法是否遵循預期的步驟。無效的結果或奇怪的執行演算法可能不會引發足以被日誌記錄的嚴重錯誤。因此,記錄這些資訊勢在必行。

提升效能  ——  我們應該記錄每個任務/子任務使用的時間和每個變數佔用的記憶體。這將有助於我們改進程式碼,進行必要的更改,優化程式碼以更快地執行,並限制記憶體消耗(或發現 Python 中常見的記憶體洩漏)。

監測工具記錄所有留存在日誌記錄中的其它資訊,這將幫助我們驗證程式碼執行的步驟,並在必要時為改進效能提供幫助。對此,資料越多越好。

3. 程式碼優化

程式碼優化包含減少時間複雜度(執行時間)和減少空間複雜度(記憶體佔用)兩方面。時間/空間複雜度通常表示成 O(x),其中 x 是關於時間或空間的多項式,這也被稱為 大 O 表示法。時間和空間複雜度被用來衡量 演算法效率

例如,假設我們有一個大小為 n 的巢狀的 for 迴圈,每次執行大約需要 2 秒,接著是一個簡單的 for 迴圈,每次執行需要 4 秒。那麼,時間消耗方程可以寫成

時間消耗 ≈ 2n²+4n = O(n²+n) = O(n²)

當使用大 O 表示法時,我們應該去掉常數項(因為 n 趨向於無窮時它可以忽略不計)以及係數。係數或縮放因子之所以被忽略,是因為我們在優化時能對其造成的影響很小。請注意,在絕對時間消耗的表示式中的係數指的是 for 環數的次數和每次執行所花費的時間的乘積,而 O(n²+n) 中的係數代表了 for 迴圈的數目(1 個雙層 for 迴圈和 1 個單層 for 迴圈)。同樣地,我們可以去掉方程中的低階項。因此,上述過程的時間複雜度為 O(n²)。

現在,我們的目標是用時間複雜度較低的方案替換程式碼的低效部分。例如,O(n) 優於 O(n²)。程式碼中最常見的時間消耗部分是 for 迴圈,最不常見但比 for 迴圈更差的是遞迴函式(時間複雜度為 O(分支^深度))。儘量用 Python 的模組或函式替換儘可能多的 for 迴圈,這些函式通常用 C 而不是 Python ,並進行過深度優化,以實現較短的執行時間。

強烈推薦你閱讀 Gayle McDowell 的程式設計師面試金典一書中的“大 O 演算法”章節。事實上,讀完整本書能夠提升你的編碼技巧。

4. 單元測試

單元測試 —— 根據功能實現程式碼測試自動化

在進入生產環境之前,你的程式碼必須通過多個測試和除錯階段。這通常分為三個層次 —— 開發、預發和生產。在一些公司中,部署到生產環境之前有一個部署到真實生產系統模擬環境的階段。當程式碼部署到生產環境時,它應該沒有任何明顯的問題,並且應該能夠處理潛在的異常。

為了能夠發現可能出現的各種各樣的問題,我們需要對不同的場景、不同的資料集、不同的邊界情況等進行測試。當我們對程式碼做了重大更改時,每次需要手動執行測試程式碼的效率是很低的。因此選擇包含一組測試用例的單元測試,並且只要我們想要測試程式碼就可以執行它。

我們必須新增具有預期結果的不同測試用例來測試我們的程式碼。單元測試模組逐個遍歷測試用例,並將程式碼的輸出與期望值進行比較。如果未達到預期結果,則測試失敗 —— 這預示著如果部署到生產環境中,你的程式碼可能會報錯。我們需要除錯程式碼然後重複該過程,直到所有測試用例都能通過。

人生苦短,因此 Python 有一個名為 unittest 的模組來實現單元測試。

5. 相容性

實際生產中很有可能的是,你的程式碼並不是獨立的函式或模組。它將被整合到公司的程式碼生態系統中,你的程式碼必須與生態系統的其他部分同步執行,而不會出現任何缺陷/故障。

例如,假設你已經開發了一種推薦演算法。整個流程通常包括從資料庫獲取最新資料、更新/生成推薦和將其儲存在資料庫中,該資料庫將被前端框架(如網頁,通過 API)讀取,來向使用者顯示推薦專案。這很簡單!這個過程就像一根鏈條,新的連結應該與前一個和後一個連結閉合,否則推薦過程就會失敗。同樣地,每個流程都必須按預期執行。

每個流程都有明確的輸入和輸出要求、預期的響應時間等等。當其他模組請求更新推薦(來自網頁)時,你的程式碼應該在可接受的時間內以所需格式返回預期值。如果結果是不符合預期的值(在購買電子產品時推薦購買牛奶)、不希望的格式(推薦以文字而不是圖片的格式展示)或是不可接受的時間(時至今日,沒有人願意等待幾分鐘來獲得推薦)—— 這暗示程式碼與系統不同步。

要避免這種情況,最佳的方法是在開始開發之前與相關團隊討論需求。如果行不通,請檢視程式碼文件(很可能會在那裡找到大量資訊),或在必要時自己編寫程式碼文件來理解需求。

6. 版本控制

Git —— 一個堪稱近年來原始碼管理中的最佳發明之一的版本控制系統,它會跟蹤計算機上程式碼的更改。跟許多現存的版本控制/跟蹤系統相比,Git 是使用最為廣泛的。

這個過程簡單地說就是“修改和提交”。我可能講得過於輕描淡寫了。這個過程有很多步驟,比如為開發建立分支、在本地提交更改、從遠端拉取檔案、將檔案推送到遠端分支,以及更多功能留待你深入探索。

每次我們對程式碼進行更改,我們不需要用不同的名稱儲存檔案,而是提交更改 —— 這意味著用新的更改覆寫舊檔案,併為這次提交賦予一個提交 ID。每當我們對程式碼進行更改時,我們通常會新增提交註釋。假如,你不喜歡上次提交中所做的更改,並希望恢復到以前的版本,通過提交 ID 可以輕鬆做到。Git 對於程式碼開發和維護來說非常有用。

你可能已經理解了版本控制對於生產系統的重要性,以及學習 Git 的必要性。為了預防新版本出現意外錯誤的情況,我們需要隨時能夠回到穩定的舊版本。

7. 可讀性

你編寫的程式碼同樣也應該易於他人理解,至少對於你的團隊成員而言。此外,如果不遵循正確的命名約定,即使你自己在編寫程式碼的幾個月後理解自己的程式碼也是很有難度的。

(i) 合適的變數名和函式名

變數和函式名稱應該是自解釋的。當有人閱讀你的程式碼時,應該很容易理解每個變數包含的內容以及每個函式的作用,至少在某種程度上如此。

給函式或變數賦一個長名稱是完全可以接受的,這個名稱要能夠明確說明其功能/角色,而不像 x、y、z 等短無意義的名稱。並且變數名稱儘量不要超過 30 個字元,函式名稱儘量不要超過 50-60 個字元。

以前,基於 IBM 標準的程式碼寬度為 80 個字元,這已經完全過時了。現在,根據 GitHub 標準大約是 120 個字元。取頁面寬度的 1/4,我們得到 30 這個足夠長但是又不會填滿頁面的變數名稱長度。函式名稱可以稍長一些,但同樣不應該填充整個頁面。因此,取頁面寬度的 1/2,我們得到 60。

例如,樣本資料中亞洲男性平均年齡的變數可以寫成 mean_age_men_Asia 而不是 agex 。類似的規則也適用於函式名稱。

(ii) 文件字串和註釋

除了合適的變數和函式名稱之外,必須在必要時提供註釋,以幫助讀者理解程式碼。

文件字串  ——  適用於函式/類/模組。函式定義中的前幾行文字描述了函式的作用及其輸入和輸出。這段文字需要用 3 個雙引號包裹起來。

def <function_name>:

"""<docstring>"""

return <output>
複製程式碼

註釋 —— 可以放在程式碼中的任何位置,以告知讀者特定行或程式碼段的作用。如果我們給變數和函式賦予合適的名稱,註釋的需求將大大減少 —— 大部分程式碼都能自我解釋。

程式碼審查:

雖然這不是編寫符合生產質量的程式碼的直接步驟,但是同行的程式碼審查將有助於提高您的編碼技巧。

沒有人能寫出完美的程式碼,除非那人有超過 10 年的經驗。程式碼總有改進的餘地。我見過有多年經驗的專業人士寫出了糟糕的程式碼,也見過正在攻讀學士學位的菜鳥擁有出色的編碼技巧 —— 你總能找到比你更優秀的人。這一切都取決於投入多少時間學習和練習,最重要的是熟能生巧。

我知道比你更優秀的人總是存在但你的團隊中不一定有。也許你是團隊中最厲害的。在這種情況下,讓團隊中的其他人測試你的程式碼並提供反饋依然可行。儘管他們並不像你那麼出色,但他們能發現一些被你忽略的東西。

當你處於職業生涯的早期階段時,程式碼審查尤為重要。它會大大提高你的編碼技巧。遵循以下步驟,來成功檢查你的程式碼。

(i) 完成所有開發、測試和除錯的程式碼編寫。確保不要犯任何低階的錯誤。然後請你的夥伴幫忙進行程式碼審查。

(ii) 把你的程式碼連結轉發給他們。一個接一個發給他們,而不要讓他們一次性審閱多個指令碼。他們為第一個指令碼提供的意見也可能適用於其他指令碼。在傳送第二個指令碼以供審閱之前,請確保在其他指令碼上應用這些更改(如果適用)。

(iii) 給他們一兩個星期來閱讀和測試每次迭代的程式碼。同時還需提供測試程式碼所需的所有資訊,如樣本輸入、限制條件等。

(iv) 與他們每個人面談並聽取他們的建議。請記住,你不必在程式碼中採納所有建議,自行選擇你認為可以改進你的程式碼的建議。

(v) 一直重複,直到你和你的團隊滿意為止。嘗試在前幾次迭代中修復或改進您的程式碼(最多 3-4 次),否則可能會留下編碼能力不足的壞印象。

希望這篇文章能對你有所幫助。

期待您的反饋。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章