概述
秦人不暇自哀,而後人哀之;後人哀之而不鑑之,亦使後人而復哀後人也! –論面向文件程式設計的重要性
如果想看見識一個人寫程式碼的功力,註釋其實是區分老司機和小鮮肉的一個顯著的分界線(有沒有觀察到你們公司的領導基本都在開會或者寫文件),通常情況下老司機的文件量與程式碼量是1:1的比例,而新人往往認為寫完功能模組就已經可以完成任務了。生產環境中需要面對現實中大量複雜的業務邏輯和資料校驗並與各方對接,文件質量和程式碼質量就被提升到了相同的高度。很多人沒有寫註釋的習慣,大多數不是因為懶惰,一方面是沒有意識到寫文件的好處,另一方面是不瞭解這方面的工具。畢竟從管理上依賴於人的主動性是遠不如依賴於工具有效的。本文介紹如何利用Python註釋提升文件書寫的質量以及效率的小技巧。
Python
在實際生產中,機器學習工作現在看起來,白天像是個演算法工程師的活,晚上就變成運維+測試了。Python 一直以來也都受到測試工程師和運維工程師的偏愛,下面是幾個經典的註釋活用case。
用註釋寫單元測試:doctest
單元測試是程式碼開發環節必不可少的一環,對於Bug定位和程式碼質量而言是非常重要的。現在最廣為人知的單元測試框架就是Unittest,它借鑑了Java中成熟的單元測試框架的JUnit。即使像Django還對這個框架有特殊的支援,然而在實現Unittest的時候會感覺確實比較囉嗦,setup,teardown…在維護單元測試的時候很多時候感覺力不從心。
一個巧妙的方式可以是通過doctest,用docstring註釋的方式來完成單元測試,由於每個方法def下面都先跟著一段測試用例,然後緊跟著就是程式碼正文,這樣一來很方便我們測試現有程式碼的質量,另一方面又便於修改。
舉個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
def factorial(n): """Return the factorial of n, an exact integer >= 0. >>> [factorial(n) for n in range(6)] [1, 1, 2, 6, 24, 120] >>> factorial(30) 265252859812191058636308480000000 >>> factorial(-1) Traceback (most recent call last): ... ValueError: n must be >= 0 Factorials of floats are OK, but the float must be an exact integer: >>> factorial(30.1) Traceback (most recent call last): ... ValueError: n must be exact integer >>> factorial(30.0) 265252859812191058636308480000000 It must also not be ridiculously large: >>> factorial(1e100) Traceback (most recent call last): ... OverflowError: n too large """ import math if not n >= 0: raise ValueError("n must be >= 0") if math.floor(n) != n: raise ValueError("n must be exact integer") if n+1 == n: # catch a value like 1e300 raise OverflowError("n too large") result = 1 factor = 2 while factor <= n: result *= factor factor += 1 return result if __name__ == "__main__": import doctest doctest.testmod() |
上面是官網提供的一個求N的階乘函式示例,在docstring 中通過 >>>符號來開始一個單元測試,之後換行輸入預期結果即可。實際上就是複製貼上一下除錯過程和結果,真的再簡單不過了,想實現TDD也因此變得非常輕鬆。
用註釋寫API文件:apidoc
在我們完成機器學習模型後,想要提供一個對外服務的介面以貢獻我們的算力時就需要完備的API文件,也是通過API的呼叫才能為我們的模型提供源源不斷的校驗資料,對於提升模型效果有非常實際的意義。對大多數人而言呼叫API來完成開發都是一件比較開心的事情,因為我們可以少做很多工作就可以實現強大功能。然而,當我們需要對外提供API時就要面臨不一樣的考驗了,介面鑑權、介面設計、版本控制、併發問題、日誌埋點…這些都是需要面對的新問題,而利用 apidoc 可以很好地解決這些API文件中常見的諸多問題,相當於通過模板提升了我們的介面設計的能力。
apidoc為Python提供了一種類似於 docstring 的方式來寫API文件,從語法上看比較類似於 R中的roxygen,都需要使用者以 @xxx 符號作為一個開頭,隨後書寫相關的定義和功能。
舉個例子:
下面是一個API介面的定義方法,最核心的部分就是
- 路由
- GET/POST方法
- 名稱/分組
- 引數與呼叫例子
(這年頭沒有用例的程式碼都是耍流氓)
1 2 3 4 5 6 7 8 9 10 |
""" @api {get} /user/:id Request User information @apiName GetUser @apiGroup User @apiParam {Number} id Users unique ID. @apiSuccess {String} firstname Firstname of the User. @apiSuccess {String} lastname Lastname of the User. """ |
我們可以直接擼一個官方示例來學習如何使用apidoc。
首先,下載示例原始碼
1 2 |
git clone https://github.com/apidoc/apidoc cd apidoc |
然後,安裝 apidoc 元件
1 |
sudo npm install apidoc -g |
接著,利用官方程式碼來製作一個例子,並且訪問即可。
1 2 |
apidoc -i example/ -o output/ -t template/ open output/index.html |
幾個引數的含義如下:
-i:input,表示輸入的資料夾
-o:output,表示輸出資料夾
-t:template,表示模板檔案,通過替換模板我們可以修改文件皮膚
在 example 資料夾下,我們需要在apidoc.json 中填寫配置檔案,定義文件的header和footer部分內容,其餘的檔案會被自動識別出其中的docstring作為API文件的一部分。
由於apidoc的官方文件非常簡單清晰,所以這裡不過多強調語法。
apidoc 還為我們提供了介面除錯的功能,在實際使用的時候要注意:
- 我們需要一個web server 才可以使用這個介面除錯的功能
- 要注意跨域的問題。
通過版本對比,我們還可以快速排查API介面的變化情況。需要注意的是這個功能要求我們要將歷史的文件記錄也要儲存在該目錄下的檔案中,通常我們可以把歷史的註釋輸出到一個特定檔案中儲存。
總的來說,雖然,API文件的書寫並不是一件難度非常高的事情,卻能體現系統模組設計和使用者體驗設計的功力,我們應該對那些無程式碼示例,無版本控制的API文件say no!
用註釋寫命令列介面:docopt
利用docopt,我們可以在註釋中直接宣告檔案的命令列傳入引數,而不需要通過 argvs變數來捕獲輸入值再做判斷,這在呼叫運維指令碼或者若干任務排程指令碼的時候尤其管用,極大地提升了CLI的效率。
舉個例子:(此處程式碼僅供參考)
1 2 3 4 5 6 7 8 9 10 11 |
"""Usage: fiannceR.py tcp <host> <port> [--timeout=<seconds>] fiannceR.py serial <port> [--baud=9600] [--timeout=<seconds>] fiannceR.py -h | --help | --version """ from docopt import docopt if __name__ == '__main__': arguments = docopt(__doc__, version='0.1.1rc') print(arguments) |
隨後,我們可以在命令列中成功呼叫
1 |
fiannceR.py tcp 0.0.0.0 3838 |
這裡的 arguments 將傳出一個字典物件,以Key-Value的形式將命令列中的輸入值捕獲。
1 2 3 4 5 6 7 8 9 |
{'--baud': None, '--help': False, '--timeout': None, '--version': False, '-h': False, '<host>': '0.0.0.0', '<port>': '3838', 'serial': False, 'tcp': True} |
總結
如果真的要從資料擼到模型、介面,那麼一排註釋的畫面真是美得不敢想象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
"""unitest >>> FinanceR('20161001') 21.01 """ def FinanceR(date): price = get_price(date) return(price) class(BaseHandler): def get(self): """apidoc @api {get} /price/:date 獲取當前價格 @apiName GetPrice @apiGroup Quota @apiParam {Number} date 交易日期 @apiSuccess {String} price """ date = self.get_argument('date',None) try: price = FinanceR(date) self.write({'data':{'price':price},'response':{'message':'success','code':200}}) except Exception as e: self.write({'data':None,'response':{'message':str(e),'code':404}}) """Usage: fiannceR.py tcp <host> <port> [--timeout=<seconds>] fiannceR.py serial <port> [--baud=9600] [--timeout=<seconds>] fiannceR.py -h | --help | --version """ from docopt import docopt if __name__ == '__main__': arguments = docopt(__doc__, version='0.1.1rc') print(arguments) |
歡迎大家留言討論,給出更多應用案例,交流分享。