<b>本文講的是來寫一個 Python 說明文檔生成器吧,</b>
<b></b>
我一開始學習 Python 的時候,我最喜歡的一件事就是坐在編譯器前,使用内置的 <code>help</code> 函數檢查類和方法,然後決定我接下來要怎麼寫。這個函數會引入一個對象并檢查其内部成員,生成說明并且輸出類似幫助文檔的内容,幫助你了解該對象的使用方法。
将 <code>help</code> 函數置入标準庫最為美妙的一點就是它能直接從代碼中輸出說明内容,這也間接地助長了一些人的懶惰,比如像我這種不願意多花時間來維護文檔的人。尤其是你已經為你的變量和函數起好了直白的名字,<code>help</code> 函數能夠給你的函數和類添加說明,也能夠通過下劃線字首正确地識别私有和受保護的成員。
在 Python 編譯器中使用 <code>help(list)</code> 會輸出以上内容
實際上,help 函數使用了 <code>pydoc</code> 子產品來生成輸出的内容,該子產品也可以在指令行中運作生成任何引入子產品的 .txt 或者 .html 格式的說明文檔。
完成最初的一些設計需求文檔之後,我想給自己開發和現行的面向其他子產品的接口添加細節描述。因為我已經給大多數的方法都寫了定義,是以我想從源檔案中自動生成引用頁面,并且想使用 markdown 格式以便日後我可以和其他檔案一起用 mkdocs 渲染成 html 文檔。
Inspect 是标準庫中的子產品,它不僅能夠讓檢視低級别的 python <code>frame</code> 和 <code>code</code> 對象,還提供了許多方法來檢查子產品和類,能夠幫助你找到可能感興趣的内容。正如上文所言, pydoc 正是用它來生成幫助文檔的。
浏覽線上文檔時,你會發現有很多相關的函數,最重要的要數 <code>getmembers()</code>、<code>getdoc()</code> 和 <code>signature()</code>,還有許多用來給<code>getmembers</code> 做篩選 <code>is...</code> 函數。通過這些函數,我們能夠很容易地周遊函數,包括區分生成器和協程,并按需遞歸到任何類及其内部。
如果我們檢視一個對象,無論什麼對象,首先要做的就是提供将其引入命名空間的結構。為什麼還要談論引入呢?鑒于我們要做的事,有許多需要考慮的事,比如虛拟環境、自定義代碼、标準子產品和重複命名。這真如一團亂麻,一招棋錯滿盤皆輸。
另一個需要銘記于心的就是代碼的執行路徑。或許會需要 <code>sys.path.append()</code> 一個目錄來擷取我們需要的子產品。我是在被檢查子產品的路徑中的一個目錄内用指令行的方式執行的,是以我将目前目錄添加到了系統路徑中,這樣就能夠解決典型的引入路徑問題。
要謹記,我們的引入函數要這樣寫:
此時,你将會在腦海中建構一個如何組織生成的 markdown 内容的藍圖。你想得到一個非遞歸至自定義類内部的淺述内容嗎?我們要對哪些方法生成描述文檔呢?内置的内容還要生成說明嗎?或者 <code>_</code> 和 <code>__</code> 方法(即非公有方法和魔術方法)?我們要如何表述函數簽名?我們要擷取注釋嗎?
我的選擇如下:
每次運作都生成一個包含遞歸至被檢視對象的各種子類内部的資訊的 <code>.md</code> 檔案
隻對我建立的自定義代碼生成說明,對引入的子產品不做處理
輸出的每個部分都必須使用 mrakdown 的二級标題(<code>##</code>)标記
所有的标題都必須包含目前描述項目的完整路徑(<code>子產品.類.子類.方法</code>)
将完整的函數簽名作為預定義格式的文本
為每個标題提供一個錨點,友善快速連結到文檔(文檔内也是如此)
任何以 <code>_</code> 或者 <code>__</code> 開始的函數都不生成文檔
引入對象之後,我們就能開始檢視它了,隻需很簡單地反複調用 <code>getmembers(對象, 篩選)</code> 方法,“篩選”即為某個 <code>is</code> 方法。你不光能使用 <code>isclass</code> 和 <code>isfunction</code> 方法,還有其他的諸如 <code>ismethod</code>,<code>isgenerator</code> 和 <code>iscoroutine</code> 方法。這完全取決于你是想寫一些能夠處理所有特殊情況的泛型,還是一些更細緻更具有特點的源碼。因為沒有什麼後顧之憂,是以我一直使用前兩個方法,并且兵分三路來分别建立子產品、類和方法的文檔格式。
當格式化一大段夾雜着程式代碼的文本的時候,我喜歡将其分為多個清單或者元組并且用 <code>"".join()</code> 來将輸出的内容組合到一起,這種寫法實際上比添寫 <code>.format</code> 和 <code>%</code> 快很多。然而,python 3.6 中新的字元串格式化方式比這種方法更快,更具可讀性。
如你所見,<code>getmembers()</code> 首先傳回了對象名,然後傳回了實際的對象,我們可以用它來遞歸整個對象結構。
我們可以用 <code>getdoc()</code> 或者 <code>getcomments()</code> 來擷取每個檢索内容的說明内容和注釋。對函數來說,我們可以使用<code>signature()</code> 擷取描述其位置和關鍵字參數、預設值以及注釋的 <code>Signature</code> 對象,并靈活生成極具描述性和風格良好的文本來幫助使用者了解我們編碼的意圖。
要注意上文中的代碼僅僅是為了讓你對結果有個直覺的認識,在大功告成之前,你還要對以下問題加以深思熟慮:
如上所示,<code>getfunctions</code> 和 <code>getclasses</code> 會展示子產品中引入的所有函數和類,包含内置和擴充包中的内容,是以你需要在 for 循環中進一步篩選。最後,我使用了目前檢視内容所在子產品的 <code>__file__</code> 屬性,換句話說,如果所檢視路徑中存在某個子產品,而子產品中定義了所檢視内容,然後我們可以使用 <code>os.path.commonprefix()</code> 将其引入。
在檔案路徑、引入結構和命名方面還存在一些疑難雜症,比如當你通過 init.py 将子產品 X 引入一個代碼包的時候,你将能通過 package.moduleX.function 的方式擷取它的函數,但是通過 moduleX.name 傳回的完整的名字卻是 package.moduleX.moduleX.function,在疊代内容的時候需要時刻牢記。
你也會從 <code>builtins</code> 中引入類,但是内置的子產品沒有 <code>__file__</code> 屬性,是以當你在添加篩選時要記得檢查哦。
因為是 markdown 文法并且我們隻是簡單的引入說明内容,是以你可以在說明文檔中引入 markdown 文法的内容,它也能很精美地顯示在頁面中。然而,這意味着你要正确操作,避免文檔說明影響 HTML 的生成。
我在 <code>sofi</code> 代碼包上運作生成器——準确來說是 <code>sofi.app</code> 子產品——以下是生成的 markdown 檔案的内容。
下面是在 mkdocs 下生成的一個 readthedocs 主題的樣本内容(不包含函數注釋):

我相信你肯定已經明白,使用這些機制自動生成的文檔能提供完整、準确、最新的子產品資訊,這使得子產品在程式設計過程中易于維護和編輯,而并非是事後諸葛。(即并非在事後已經不需要的時候才總結出一份與子產品相符的文檔,原文為 instead of after the fact ,譯者注)。我強烈建議每個人都試一試。
本文結束之前,我想回顧一下并說明 makdocs 并不是唯一的文檔包,還有很多著名且應用廣泛的文檔包,比如 Sphinx(mkdocs 既是基于此)和 Doxygen,兩者都可以實作我們今天談論的内容。然而,我一如既往義無反顧地這樣做,就是為了能夠深入了解 Python 和它所自帶的工具。
<b>原文釋出時間為:2016年12月17日</b>
<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>